<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: LazyDev_OH</title>
    <description>The latest articles on Forem by LazyDev_OH (@lazydev_oh).</description>
    <link>https://forem.com/lazydev_oh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3868972%2F81efd9f9-64e0-4189-93c4-9a8b3a18fff8.png</url>
      <title>Forem: LazyDev_OH</title>
      <link>https://forem.com/lazydev_oh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lazydev_oh"/>
    <language>en</language>
    <item>
      <title>axios npm Supply Chain Attack (March 31, 2026) — What Happened and How to Check Your Lock File Right Now</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Tue, 14 Apr 2026 04:44:20 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/axios-npm-supply-chain-attack-march-31-2026-what-happened-and-how-to-check-your-lock-file-coh</link>
      <guid>https://forem.com/lazydev_oh/axios-npm-supply-chain-attack-march-31-2026-what-happened-and-how-to-check-your-lock-file-coh</guid>
      <description>&lt;p&gt;On &lt;strong&gt;March 31, 2026&lt;/strong&gt;, malicious versions of &lt;code&gt;axios&lt;/code&gt; — a package with &lt;strong&gt;70M+ weekly downloads&lt;/strong&gt; — were published to npm after the maintainer's account was hijacked via social engineering. Versions &lt;code&gt;1.14.1&lt;/code&gt; and &lt;code&gt;0.30.4&lt;/code&gt; were pushed back-to-back, both carrying a &lt;code&gt;plain-crypto-js@^4.2.1&lt;/code&gt; dependency that deploys a &lt;strong&gt;cross-platform RAT&lt;/strong&gt; through a postinstall hook.&lt;/p&gt;

&lt;p&gt;The malicious releases sat on the registry for roughly &lt;strong&gt;3 hours&lt;/strong&gt;. In that window, an estimated &lt;strong&gt;600,000 installs&lt;/strong&gt; occurred.&lt;/p&gt;

&lt;p&gt;If you use axios, check your lock file. Now.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Malicious: &lt;code&gt;axios@1.14.1&lt;/code&gt;, &lt;code&gt;axios@0.30.4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Safe: &lt;code&gt;axios@1.14.0&lt;/code&gt;, &lt;code&gt;axios@0.30.3&lt;/code&gt; (pre-incident), &lt;code&gt;1.15.0+&lt;/code&gt; / &lt;code&gt;0.30.5+&lt;/code&gt; (post-incident)&lt;/li&gt;
&lt;li&gt;Attribution: North Korea — Sapphire Sleet (Microsoft) / UNC1069 (Google)&lt;/li&gt;
&lt;li&gt;Action: wipe &lt;code&gt;node_modules&lt;/code&gt;, reinstall, &lt;strong&gt;rotate all credentials&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3i3ozbu7ofnuvaxmgir8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3i3ozbu7ofnuvaxmgir8.png" alt="axios supply chain attack timeline" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Check Right Now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Installed axios version&lt;/span&gt;
npm list axios

&lt;span class="c"&gt;# Check lock file for malicious versions&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"axios@(1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4)|plain-crypto-js"&lt;/span&gt; package-lock.json

&lt;span class="c"&gt;# Monorepo-wide scan&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"package-lock.json"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;1.14.1&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;0.30.4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If grep returns a match, remediate immediately. No output means you're probably fine — but also check git history. If the malicious version was ever installed in the past, the postinstall hook has already run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Did the malicious version ever land in lock file history?&lt;/span&gt;
git log &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; package-lock.json | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4|plain-crypto-js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;pnpm&lt;/code&gt;, use &lt;code&gt;pnpm list axios&lt;/code&gt;; with &lt;code&gt;yarn&lt;/code&gt;, &lt;code&gt;yarn list --pattern axios&lt;/code&gt;. The lock-file grep pattern applies regardless of package manager.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 3-Hour Timeline
&lt;/h2&gt;

&lt;p&gt;Independent reconstructions from &lt;a href="https://www.aikido.dev/blog/axios-npm-compromised-maintainer-hijacked-rat" rel="noopener noreferrer"&gt;Aikido Security&lt;/a&gt;, Arctic Wolf, and &lt;a href="https://www.elastic.co/security-labs/axios-one-rat-to-rule-them-all" rel="noopener noreferrer"&gt;Elastic Security Labs&lt;/a&gt; largely agree:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (UTC)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2026-03-31 00:21&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;axios@1.14.1&lt;/code&gt; published — targets 1.x line, adds &lt;code&gt;plain-crypto-js@^4.2.1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;+39 min&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Attacker stages the 0.x legacy release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;01:00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;axios@0.30.4&lt;/code&gt; published — 0.x branch compromised&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;~03:00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Socket.dev / Aikido detect anomalous postinstall hook, community alerts begin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;~04:00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;npm force-unpublishes both versions, exposure totals ~3 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Only 3 hours" is a dangerous framing. Vercel, GitHub Actions, CircleCI, and similar CI environments pull fresh versions on cache misses every 10~30 seconds. Globally, tens of thousands of builds ran in that window. Several regions also reported CDN cache serving the malicious version briefly after the unpublish.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Malicious Code Works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;plain-crypto-js&lt;/code&gt; disguises itself as a crypto utility. &lt;strong&gt;It is never imported anywhere in axios source&lt;/strong&gt; — it exists solely to execute its postinstall hook.&lt;/p&gt;

&lt;p&gt;During install, npm runs &lt;code&gt;postinstall&lt;/code&gt; automatically. That hook contacts the attacker's C2 server and pulls a second-stage payload. The payload detects the host OS (macOS / Windows / Linux) and drops a matching RAT (Remote Access Trojan).&lt;/p&gt;

&lt;p&gt;Per Elastic Security Labs, the C2 protocol rides on HTTPS with a custom command set designed to blend into normal API traffic, making network-level detection difficult.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Attack Vector — Maintainer Account Hijack
&lt;/h2&gt;

&lt;p&gt;Per SANS Institute and &lt;a href="https://thehackernews.com/2026/04/unc1069-social-engineering-of-axios.html" rel="noopener noreferrer"&gt;The Hacker News&lt;/a&gt;, the axios maintainer account was hijacked through a &lt;strong&gt;targeted social engineering campaign&lt;/strong&gt;. The attacker changed the account email to &lt;code&gt;ifstap@proton.me&lt;/code&gt;, then abused publish permissions to push the two malicious releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attribution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft Threat Intelligence&lt;/strong&gt;: &lt;a href="https://www.microsoft.com/en-us/security/blog/2026/04/01/mitigating-the-axios-npm-supply-chain-compromise/" rel="noopener noreferrer"&gt;Sapphire Sleet&lt;/a&gt; — North Korea state actor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google GTIG&lt;/strong&gt;: &lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/north-korea-threat-actor-targets-axios-npm-package" rel="noopener noreferrer"&gt;UNC1069&lt;/a&gt; — same actor, tracked independently&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Joint attribution confirmed&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;UNC1069 / Sapphire Sleet has a track record of targeting developers through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fake job offers with malicious coding-test files&lt;/li&gt;
&lt;li&gt;Fake recruiter outreach via LinkedIn or Telegram&lt;/li&gt;
&lt;li&gt;Phishing open-source maintainers directly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This axios case appears to fall into the third pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Remediation
&lt;/h2&gt;

&lt;p&gt;Don't just upgrade — wipe and rebuild.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Wipe node_modules + lock file&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; node_modules package-lock.json

&lt;span class="c"&gt;# 2. Clean cache&lt;/span&gt;
npm cache clean &lt;span class="nt"&gt;--force&lt;/span&gt;

&lt;span class="c"&gt;# 3. Reinstall latest safe version&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;axios@latest

&lt;span class="c"&gt;# 4. Verify&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js"&lt;/span&gt; package-lock.json
&lt;span class="c"&gt;# → No output = clean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the same to deployment environments (Vercel / Netlify / GitHub Actions caches). A stale cache can still serve the compromised artifact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rotate All Credentials — Not Just Env Vars
&lt;/h2&gt;

&lt;p&gt;If a malicious version ever reached your machines, the RAT may still be resident. The attacker has system-level access, not just &lt;code&gt;process.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotation checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] AWS / GCP / Azure access keys&lt;/li&gt;
&lt;li&gt;[ ] AI API keys — OpenAI / Anthropic / Gemini&lt;/li&gt;
&lt;li&gt;[ ] Database passwords — PostgreSQL, MySQL, MongoDB&lt;/li&gt;
&lt;li&gt;[ ] Payment API keys — Stripe, LemonSqueezy, Paddle&lt;/li&gt;
&lt;li&gt;[ ] GitHub Personal Access Token + SSH keys&lt;/li&gt;
&lt;li&gt;[ ] App secrets — &lt;code&gt;NEXTAUTH_SECRET&lt;/code&gt;, &lt;code&gt;SESSION_SECRET&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Webhook secrets for external services&lt;/li&gt;
&lt;li&gt;[ ] Infected-machine SSH public keys — remove from &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; on any servers they reached&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Revoke old keys immediately after issuing new ones. Keeping the old key alive defeats the rotation.&lt;/p&gt;

&lt;p&gt;For machines with high suspicion of compromise, an OS reinstall is the safest option. CI runner images should be rebuilt clean. Local dev machines should at minimum clear browser sessions, SSH keys, and saved AWS CLI profiles, then reconfigure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prevention Routines
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Commit lock files.&lt;/strong&gt; Without a lock file, every build can pull a different version. If &lt;code&gt;package-lock.json&lt;/code&gt; is in &lt;code&gt;.gitignore&lt;/code&gt;, remove it now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Put &lt;code&gt;npm audit&lt;/code&gt; in CI.&lt;/strong&gt; Run it on every PR. &lt;code&gt;npm audit --audit-level=high&lt;/code&gt; catches high-severity issues at minimum. Caveat: audit only sees what's public in the CVE database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Tighten version range specifiers.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;❌&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Too&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;loose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;opens&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;door&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;auto-updates&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.13.0"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;✅&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Exact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.14.0"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;✅&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Patch-only&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~1.14.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Monitor beyond CVE.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependabot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built into GitHub&lt;/td&gt;
&lt;td&gt;CVE-based, limited against fresh attacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Socket.dev&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Behavioral analysis&lt;/td&gt;
&lt;td&gt;Flagged this axios incident early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aikido Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time behavioral&lt;/td&gt;
&lt;td&gt;Published first public analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snyk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scan + remediation&lt;/td&gt;
&lt;td&gt;Free tier available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;npm audit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;CVE-based limits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Realistic combo: Dependabot + Socket.dev. Single-tool reliance leaves blind spots.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Keeps Happening
&lt;/h2&gt;

&lt;p&gt;The npm ecosystem has a low publishing bar. A single account compromise can poison a package used by hundreds of millions of developers. That structural fact isn't changing fast.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XZ Utils&lt;/strong&gt; (2024-03) — compromised Linux distribution backdoor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;event-stream&lt;/strong&gt; (2018) — crypto wallet stealer hidden in dependency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ua-parser-js&lt;/strong&gt; (2021) — malicious versions with credential stealer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;axios&lt;/strong&gt; (2026-03) — this incident&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;axios isn't the first and won't be the last.&lt;/p&gt;

&lt;p&gt;Following this incident, npm is reportedly considering mandatory 2FA expansion and a 24-hour cooldown on maintainer email changes. GitHub already required 2FA for top npm maintainers since 2024, but &lt;strong&gt;this hijack went through the email recovery flow&lt;/strong&gt;. Security chains only hold as strong as the weakest link.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check your lock file right now&lt;/strong&gt; — don't assume you're fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wipe, don't just upgrade&lt;/strong&gt; — stale caches and remnant RATs are real risks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate credentials broadly&lt;/strong&gt; — system-level access means everything is suspect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put behavioral analysis in your CI&lt;/strong&gt; — CVE-based tools can't catch fresh attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin exact versions for critical packages&lt;/strong&gt; — range specifiers are attack surface.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Trusting a popular package and verifying it are different things. If you use axios, put a version check in your routine starting today.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/axios/axios/issues/10636" rel="noopener noreferrer"&gt;axios Official Post-Mortem (GitHub #10636)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.microsoft.com/en-us/security/blog/2026/04/01/mitigating-the-axios-npm-supply-chain-compromise/" rel="noopener noreferrer"&gt;Microsoft Security Blog — Mitigating the Axios npm supply chain compromise&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/north-korea-threat-actor-targets-axios-npm-package" rel="noopener noreferrer"&gt;Google Cloud Threat Intelligence — UNC1069 analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.aikido.dev/blog/axios-npm-compromised-maintainer-hijacked-rat" rel="noopener noreferrer"&gt;Aikido Security — first public analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.elastic.co/security-labs/axios-one-rat-to-rule-them-all" rel="noopener noreferrer"&gt;Elastic Security Labs — RAT technical analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://socket.dev/blog/axios-npm-package-compromised" rel="noopener noreferrer"&gt;Socket.dev — plain-crypto-js analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://snyk.io/blog/axios-npm-package-compromised-supply-chain-attack-delivers-cross-platform/" rel="noopener noreferrer"&gt;Snyk Security Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-axios-npm-supply-chain-attack-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt; — April 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>npm</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Vercel vs Netlify vs Cloudflare Pages 2026 — Deep Comparison with Real Numbers</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Tue, 14 Apr 2026 04:25:43 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/vercel-vs-netlify-vs-cloudflare-pages-2026-deep-comparison-with-real-numbers-8pl</link>
      <guid>https://forem.com/lazydev_oh/vercel-vs-netlify-vs-cloudflare-pages-2026-deep-comparison-with-real-numbers-8pl</guid>
      <description>&lt;p&gt;The web deployment landscape crystallized into a clear three-way split in 2026. Vercel for Next.js full-stack. Cloudflare Pages for static sites and edge workloads. Netlify for the Jamstack middle ground. All three ship with &lt;code&gt;git push&lt;/code&gt;-to-deploy out of the box.&lt;/p&gt;

&lt;p&gt;The real story is in billing and performance. In February 2026, Vercel shipped Fluid Compute to GA and announced &lt;strong&gt;up to 95% cost savings across 45 billion weekly requests&lt;/strong&gt;. Cloudflare Workers hold cold starts under 5ms. Netlify migrated to credit-based billing in September 2025. The same app gets billed differently, responds at different speeds, and feels different to operate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version&lt;/strong&gt;: Next.js ecosystem → Vercel. High-traffic static or edge-heavy → Cloudflare Pages. Forms and adapter ecosystem → Netlify.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmlnxs4r4cj8i789ctyc1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmlnxs4r4cj8i789ctyc1.png" alt="Vercel vs Netlify vs Cloudflare Pages comparison" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt;: Hobby free (100GB · 1M invocations), Pro $20/user/mo, Fluid Compute saves up to 95%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify&lt;/strong&gt;: Free 100GB · 300 build min, Pro $19/user/mo, credit-based since Sept 2025&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Pages&lt;/strong&gt;: unlimited bandwidth, 500 builds/mo free, Workers Paid $5/mo bundles ecosystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold starts&lt;/strong&gt;: Cloudflare &amp;lt; 5ms &amp;gt; Vercel Fluid ~0ms (warm) &amp;gt; Netlify 150~3,000ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js support&lt;/strong&gt;: Vercel native &amp;gt; Netlify adapter (30~60% slower builds) &amp;gt; Cloudflare OpenNext (constraints)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge PoPs&lt;/strong&gt;: Cloudflare 330+ / Vercel 40+ / Netlify 8-region multi-cloud&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare ecosystem&lt;/strong&gt;: KV · D1 · R2 · Durable Objects · Hyperdrive bundled at $5&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Each Platform Actually Is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Vercel&lt;/strong&gt; was founded by Guillermo Rauch in 2015 — the same person behind Next.js. As of 2026, the company sits around $3.2B valuation. The core edge: native Next.js integration. ISR, Image Optimization, Middleware, Server Actions, Cache Components — all of it works with zero config. Hobby plan is personal / non-commercial only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netlify&lt;/strong&gt; was founded in 2014 and coined the term "Jamstack." Framework adapters span Astro, Next.js, SvelteKit, Nuxt, Gatsby, Hugo — the widest ecosystem of the three. Forms, serverless functions, and Edge Functions come built in. In September 2025, they migrated to credit-based billing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Pages&lt;/strong&gt; runs on Cloudflare's global edge network. The headline features are 330+ PoPs and unlimited bandwidth. Workers Paid ($5/month) alone bundles Workers, Pages Functions, KV, D1, R2, Durable Objects, and Hyperdrive. Next.js runs through OpenNext and inherits edge runtime constraints — some Node.js modules unavailable, ISR limited.&lt;/p&gt;




&lt;h2&gt;
  
  
  Free Tier Deep Dive
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Vercel Hobby&lt;/th&gt;
&lt;th&gt;Netlify Free&lt;/th&gt;
&lt;th&gt;Cloudflare Pages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bandwidth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100GB&lt;/td&gt;
&lt;td&gt;100GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unlimited deploys&lt;/td&gt;
&lt;td&gt;300 min/mo&lt;/td&gt;
&lt;td&gt;500 builds/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Function invocations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1M/mo&lt;/td&gt;
&lt;td&gt;125K/mo&lt;/td&gt;
&lt;td&gt;100K/day (~3M/mo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4h Active CPU&lt;/td&gt;
&lt;td&gt;Credit-based&lt;/td&gt;
&lt;td&gt;10ms/request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1GB Blob&lt;/td&gt;
&lt;td&gt;10GB&lt;/td&gt;
&lt;td&gt;R2 10GB / KV 1GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Usage restriction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Personal / non-commercial&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commercial OK&lt;/td&gt;
&lt;td&gt;Commercial OK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloudflare dominates on raw bandwidth — traffic spikes don't trigger overage invoices. Vercel Hobby's decisive constraint is the no-commercial clause. A single advertisement can put you in violation. Netlify's 300 build-minute cap is the actual bottleneck — a medium Next.js project often builds in 5~8 minutes, hitting the ceiling at 40~60 deploys/month.&lt;/p&gt;




&lt;h2&gt;
  
  
  Paid Plans and Overage Simulation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vercel Pro&lt;/strong&gt;: $20/user/month + 16 CPU-hours, 1,440 GB-hours memory — overage Active CPU $0.128/hour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify Pro&lt;/strong&gt;: $19/user/month + 1TB bandwidth, 25K build minutes — $7 per 500 extra build min, $20 per 100GB extra bandwidth, $25 per 1M extra invocations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Workers Paid&lt;/strong&gt;: $5/month + 10M requests, 30M CPU-ms — $0.30 per extra 1M requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scenario A — Small blog (100K monthly visits, 50GB bandwidth, 500K function calls)&lt;/strong&gt;&lt;br&gt;
Vercel Hobby $0 / Pro $20. Netlify Free possible. Cloudflare Pages $0. → &lt;strong&gt;Cloudflare Pages wins&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario B — Next.js SaaS (500K monthly visits, 5M function calls, DB-heavy)&lt;/strong&gt;&lt;br&gt;
Vercel Pro ~$20~30 (Fluid keeps CPU overage near zero). Netlify Pro $19 + function overage $100 = &lt;strong&gt;$119&lt;/strong&gt;. Cloudflare Workers Paid $5 + extra requests $1.50 = &lt;strong&gt;$6.50&lt;/strong&gt;. → Order: Cloudflare, Vercel, Netlify. If Next.js compatibility is non-negotiable, Vercel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario C — Image hosting (2TB monthly downloads)&lt;/strong&gt;&lt;br&gt;
Vercel Pro $20 + 1.9TB overage $380 = &lt;strong&gt;$400&lt;/strong&gt;. Netlify Pro $19 + 1TB overage $200 = &lt;strong&gt;$219&lt;/strong&gt;. Cloudflare R2 $0 egress + $30 storage = &lt;strong&gt;$30&lt;/strong&gt;. → For egress-heavy workloads, Cloudflare is effectively the only option.&lt;/p&gt;


&lt;h2&gt;
  
  
  Vercel Fluid Compute — Real Savings
&lt;/h2&gt;

&lt;p&gt;Fluid Compute hit GA in February 2026. Per Vercel's figures: 45B weekly requests, customers seeing up to 95% savings, 75%+ of all functions now on Fluid. The old model billed the entire function duration. Fluid only bills &lt;strong&gt;Active CPU windows&lt;/strong&gt; — when your code is actually executing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: Next.js API handler (I/O-bound)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 100ms — JSON parsing, validation (Active CPU)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;// 400ms — Supabase query wait (I/O, Fluid bills nothing)&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;// 30ms — response serialization (Active CPU)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Total wall time: 530ms&lt;/span&gt;
&lt;span class="c1"&gt;// Legacy billing: 530ms (all of it)&lt;/span&gt;
&lt;span class="c1"&gt;// Fluid billing: 130ms (Active CPU only) → 75% saved&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From Vercel's case studies: &lt;em&gt;"Many of our API endpoints were lightweight and involved external requests, resulting in idle compute time. By leveraging in-function concurrency, we were able to share compute resources between invocations, cutting costs by over 50% with zero code changes."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For typical Next.js apps, expect function invocation counts to drop 30~50% with proportional cost reduction. The benefit is limited for CPU-heavy workloads (ML inference, image resizing) where Active CPU dominates.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cold Start Benchmarks — 5ms vs 250ms vs 3 Seconds
&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;Cold start&lt;/th&gt;
&lt;th&gt;Warm response&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Workers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 5ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1ms&lt;/td&gt;
&lt;td&gt;V8 Isolates + Shard-and-Conquer (99.99% warm)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vercel Fluid&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~0ms (warm)&lt;/td&gt;
&lt;td&gt;20~50ms&lt;/td&gt;
&lt;td&gt;Instance pre-warming + in-function concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vercel legacy serverless&lt;/td&gt;
&lt;td&gt;~250ms&lt;/td&gt;
&lt;td&gt;50~80ms&lt;/td&gt;
&lt;td&gt;AWS Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Netlify Functions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;150~3,000ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80~150ms&lt;/td&gt;
&lt;td&gt;AWS Lambda (high variance)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cloudflare Workers' sub-5ms comes from V8 Isolates. Instead of spinning up a container, the platform runs your function directly inside the JavaScript engine. Initialization overhead is near zero. Shard-and-Conquer consistent hashing routes same-request traffic to the same node, keeping warm-hit rate at 99.99%.&lt;/p&gt;

&lt;p&gt;Vercel Fluid keeps instances warm with in-function concurrency — a single instance handles multiple concurrent requests. Near-zero cold starts for active functions.&lt;/p&gt;

&lt;p&gt;Netlify, running on AWS Lambda, is the slowest. Cold starts up to 3 seconds in benchmarks. For low-traffic sites or early-morning first requests, users feel the wait.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next.js Feature Compatibility Matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Next.js feature&lt;/th&gt;
&lt;th&gt;Vercel&lt;/th&gt;
&lt;th&gt;Netlify&lt;/th&gt;
&lt;th&gt;Cloudflare (OpenNext)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server Components (RSC)&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server Actions&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISR (revalidate)&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;On-Demand only&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image Optimization&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Adapter&lt;/td&gt;
&lt;td&gt;Cloudflare Images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middleware&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full (edge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache Components&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial Prerendering (PPR)&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge Runtime&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Edge Functions&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Node.js modules&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;All&lt;/td&gt;
&lt;td&gt;Some blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build speed (same project)&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;30~60% slower&lt;/td&gt;
&lt;td&gt;20% slower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Next.js' latest features (Cache Components, PPR) only ship fully on Vercel. Netlify covers most of it via adapter, but ISR semantics differ and builds run noticeably longer. Cloudflare Pages inherits edge-runtime constraints — can't use &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;net&lt;/code&gt;, or &lt;code&gt;child_process&lt;/code&gt;, and ISR requires wiring Incremental Cache into KV separately.&lt;/p&gt;

&lt;p&gt;On the flip side, Cloudflare's Image Optimization routes through Cloudflare Images (faster CDN), and Edge Runtime is native. For edge-friendly codebases, Cloudflare Pages can actually win.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cloudflare Ecosystem — KV · D1 · R2 · Durable Objects
&lt;/h2&gt;

&lt;p&gt;Cloudflare's real edge: $5/month Workers Paid bundles 6+ data services. Each as a standalone SaaS would run into hundreds of dollars.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Price (Paid)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workers KV&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Global key-value, config/session/personalization&lt;/td&gt;
&lt;td&gt;Reads 10M $0.50, writes 1M $5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed SQLite, lightweight relational DB&lt;/td&gt;
&lt;td&gt;Reads 25M $1, writes 50K $1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;R2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;S3-compatible object storage, &lt;strong&gt;zero egress&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;$0.015/GB storage, Class A 1M $4.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Durable Objects&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;WebSockets, collaboration, locks, rate limiters&lt;/td&gt;
&lt;td&gt;1M requests $0.15, $0.20/GB/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queues&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Message queue, async work&lt;/td&gt;
&lt;td&gt;1M operations $0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hyperdrive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;External PostgreSQL pooling&lt;/td&gt;
&lt;td&gt;Included in Workers Paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Practical combo: sessions/config on KV, user data on D1, images/files on R2, chat rooms on Durable Objects, background jobs via Queues. Everything at the same $5.&lt;/p&gt;

&lt;p&gt;AWS equivalent stack: RDS ($15) + DynamoDB ($10) + S3 ($5) + &lt;strong&gt;egress ($100+)&lt;/strong&gt; + SQS ($2) = &lt;strong&gt;$130+/month minimum&lt;/strong&gt;. R2's zero-egress policy alone makes file-heavy services land in a completely different cost range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durable Objects is the only practical choice for stateful edge computing.&lt;/strong&gt; WebSocket chat rooms, Google Docs-style real-time collaboration, distributed locks, rate limiters. Vercel and Netlify have no equivalent, forcing external services (Pusher, Ably) to fill the gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Edge Network and Global TTFB
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt;: 330+ PoPs across 120+ countries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt;: own edge network (40+ regions) + AWS/GCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify&lt;/strong&gt;: multi-cloud AWS/GCP/Azure (8 main regions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TTFB benchmarks from Korea (static content): Cloudflare Seoul PoP 30~50ms, Vercel Tokyo/Seoul region 80~120ms, Netlify US-West default 250~400ms. For global apps with APAC users, Cloudflare is overwhelmingly the fastest experience.&lt;/p&gt;

&lt;p&gt;Vercel's Tokyo/Singapore regions can reach ~100ms in Korea when explicitly configured. Hobby has limited region pinning; Pro enables per-project region selection. Setting &lt;code&gt;regions&lt;/code&gt; in &lt;code&gt;vercel.json&lt;/code&gt; is important — defaults often point to US regions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Netlify Credit-Based Pricing
&lt;/h2&gt;

&lt;p&gt;Since September 2025, Netlify uses a unified credit pool. Approximate conversions: 1 build minute = 1 credit, 1,000 function invocations = 1 credit, 1 GB bandwidth = 1 credit. Pro includes 500 credits/month — in theory 100 deploys if builds are 5 minutes each, but practical ceiling drops to 50~70 after other usage.&lt;/p&gt;

&lt;p&gt;The complaint is predictability. &lt;em&gt;"My build ran long and drained my credits"&lt;/em&gt; posts keep showing up in dev forums. Accounts created before September 4, 2025 can stay on the legacy plan.&lt;/p&gt;

&lt;p&gt;Netlify's strengths still hold — Forms built in (100 submissions/mo free), Identity, Large Media, Split Testing. Features Vercel and Cloudflare don't match natively.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Vercel&lt;/th&gt;
&lt;th&gt;Netlify&lt;/th&gt;
&lt;th&gt;Cloudflare Pages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free bandwidth&lt;/td&gt;
&lt;td&gt;100GB&lt;/td&gt;
&lt;td&gt;100GB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid starting&lt;/td&gt;
&lt;td&gt;$20/user/mo&lt;/td&gt;
&lt;td&gt;$19/user/mo&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5/mo&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start&lt;/td&gt;
&lt;td&gt;~0ms (warm)&lt;/td&gt;
&lt;td&gt;150~3,000ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&amp;lt; 5ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js support&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Native (full)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adapter (mostly)&lt;/td&gt;
&lt;td&gt;OpenNext (constrained)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless billing&lt;/td&gt;
&lt;td&gt;Active CPU (Fluid)&lt;/td&gt;
&lt;td&gt;Credit-based&lt;/td&gt;
&lt;td&gt;Per-request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global PoPs&lt;/td&gt;
&lt;td&gt;40+ edge&lt;/td&gt;
&lt;td&gt;8 regions&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;330+ PoPs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commercial free use&lt;/td&gt;
&lt;td&gt;Not allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;td&gt;Allowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ecosystem&lt;/td&gt;
&lt;td&gt;Next.js + Postgres/KV/Blob&lt;/td&gt;
&lt;td&gt;Forms, Identity, Split Testing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;KV, D1, R2, DO, Queues&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build speed (Next.js)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fastest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30~60% slower&lt;/td&gt;
&lt;td&gt;20% slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DX / dashboard&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Best&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;Deep but learning curve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Egress cost&lt;/td&gt;
&lt;td&gt;Deducts from bandwidth&lt;/td&gt;
&lt;td&gt;Deducts&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;R2 $0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Combining Platforms — Real-World Patterns
&lt;/h2&gt;

&lt;p&gt;No reason to pick one and stick with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Pattern A — Subdomain split (most common)
static.example.com  → Cloudflare Pages (images, docs, heavy assets)
app.example.com     → Vercel Pro (Next.js full-stack)
forms.example.com   → Netlify (form intake)

# Pattern B — Cloudflare as front CDN, Vercel as origin
Cloudflare (CDN/WAF/DDoS) → Vercel (serverless origin)
# Cloudflare absorbs egress, Vercel handles execution only

# Pattern C — Full Cloudflare stack (AWS alternative)
Cloudflare Pages + Workers + D1 + R2 + Durable Objects
# Full-stack infra starting at $5/month
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Recommendations by Scenario
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next.js full-stack SaaS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Vercel Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fluid Compute 95% savings, Cache Components/PPR native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image / video hosting&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare + R2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero egress, 330+ PoPs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Astro / SvelteKit&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Netlify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adapter ecosystem, built-in forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time / WebSocket&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare + DO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only edge stateful solution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global TTFB matters&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Largest edge network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intermittent traffic&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Vercel Fluid / Cloudflare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low cold start&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Personal (no revenue)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Vercel Hobby&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;All Next.js features free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form-heavy marketing&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Netlify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Forms built in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;All three platforms are mature as of 2026. "Which is better" is the wrong frame — "which fits your stack" is the real question.&lt;/p&gt;

&lt;p&gt;Three trends worth watching as of April 2026:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vercel Fluid Compute&lt;/strong&gt; now powers 75%+ of all Vercel Functions and has measurably dropped Next.js full-stack bills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare D1&lt;/strong&gt; moved past GA with real production references, making AWS RDS replacement a concrete option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netlify's credit-based pricing&lt;/strong&gt; is driving heavy users to reconsider.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The right choice shifts each year. Review your workload periodically.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Official sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vercel.com/pricing" rel="noopener noreferrer"&gt;Vercel Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/docs/fluid-compute" rel="noopener noreferrer"&gt;Vercel Fluid Compute docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/blog/introducing-active-cpu-pricing-for-fluid-compute" rel="noopener noreferrer"&gt;Vercel Active CPU pricing blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netlify.com/pricing/" rel="noopener noreferrer"&gt;Netlify Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/" rel="noopener noreferrer"&gt;Netlify credit-based pricing docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/workers/platform/pricing/" rel="noopener noreferrer"&gt;Cloudflare Workers pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/durable-objects/platform/pricing/" rel="noopener noreferrer"&gt;Cloudflare Durable Objects pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.cloudflare.com/unpacking-cloudflare-workers-cpu-performance-benchmarks/" rel="noopener noreferrer"&gt;Cloudflare Workers CPU benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-vercel-vs-netlify-vs-cloudflare-pages-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt; — April 2026 pricing. Plans and policies change frequently; verify with official docs before committing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vercel</category>
      <category>cloudflare</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Catalogued the Security Patterns That Keep Showing Up in AI Code</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Mon, 13 Apr 2026 04:03:48 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/i-catalogued-the-security-patterns-that-keep-showing-up-in-ai-code-2jla</link>
      <guid>https://forem.com/lazydev_oh/i-catalogued-the-security-patterns-that-keep-showing-up-in-ai-code-2jla</guid>
      <description>&lt;p&gt;Across the Apsity App Store dashboard, the FeedMission SaaS, and a dozen side projects, more than half the code I touch is AI-generated. After &lt;a href="https://gocodelab.com/en/blog/en-feedmission-saas-7days-mvp-ep04" rel="noopener noreferrer"&gt;shipping a SaaS in 7 days&lt;/a&gt;, vibe coding has been the default workflow.&lt;/p&gt;

&lt;p&gt;Run it long enough and the patterns show up. AI-generated code keeps producing the same classes of security holes. One FeedMission review surfaced &lt;a href="https://gocodelab.com/en/blog/en-feedmission-nextjs-security-email-debug-ep06" rel="noopener noreferrer"&gt;seven criticals at the same time&lt;/a&gt; — a Slack webhook URL bundled into the frontend, an unsubscribe endpoint that any email address could trigger, an admin reply leaking through a public API, routes missing team-member auth checks. None of that was bad luck. Industry research lists these as the highest-frequency patterns, and they had effectively reproduced themselves in our codebase.&lt;/p&gt;

&lt;p&gt;So now I run the same seven checks before every deploy, the same way each time. This post is the pattern catalogue plus the routine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, first
&lt;/h2&gt;

&lt;p&gt;This isn't a vibe check. Multiple groups in 2026 (Georgia Tech, Cloud Security Alliance, Checkmarx) analyzed AI-generated code and found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;40–62%&lt;/strong&gt; of samples contain security issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2.74×&lt;/strong&gt; more vulnerable than human-written code on equivalent tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;86%&lt;/strong&gt; failed XSS defenses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;88%&lt;/strong&gt; vulnerable to log injection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;35 new CVEs&lt;/strong&gt; tied to AI-generated code in March 2026 alone&lt;/li&gt;
&lt;li&gt;One AI app leaked &lt;strong&gt;1.5M API keys&lt;/strong&gt; post-launch — shipped without security review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nobody's quitting vibe coding because of these numbers. I'm not. But the 10 minutes you spend before deploy is what decides production's fate.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI skips security
&lt;/h2&gt;

&lt;p&gt;Beginners get this wrong. The AI didn't make a mistake — it built what you asked for. "Make a user profile API" → it makes one. Auth wasn't requested, so it's not there. It leaves &lt;code&gt;// TODO: add auth here&lt;/code&gt; and moves on.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;put security in the prompt from the start.&lt;/strong&gt; "Include JWT auth middleware, read secrets only from env, no raw SQL, no TODO comments, ship complete code." One line changes the output quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Top 7 mistakes — in the order I hit them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Red flag&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Hardcoded API keys&lt;/td&gt;
&lt;td&gt;Scraped by bots within seconds&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sk_&lt;/code&gt;, &lt;code&gt;api_key=&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Auth-less API routes&lt;/td&gt;
&lt;td&gt;URL-only access to your DB&lt;/td&gt;
&lt;td&gt;no session/auth/token references&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; misuse&lt;/td&gt;
&lt;td&gt;Service-role key in browser bundle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NEXT_PUBLIC_*_SECRET/KEY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Raw SQL interpolation&lt;/td&gt;
&lt;td&gt;SQL injection → full DB exfil&lt;/td&gt;
&lt;td&gt;&lt;code&gt;`SELECT ... ${}`&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;CORS wildcards&lt;/td&gt;
&lt;td&gt;Any domain hits your API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Allow-Origin: *&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Missing XSS / log-injection defense&lt;/td&gt;
&lt;td&gt;User input straight into HTML/logs&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;, raw-string logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Phantom packages (slopsquatting)&lt;/td&gt;
&lt;td&gt;Malicious package under hallucinated name&lt;/td&gt;
&lt;td&gt;unfamiliar packages, low downloads&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  1 and #3 hit fastest. The moment you push to GitHub, scraper bots scoop the key and burn your API quota. If you've never been hit, you've only been lucky.
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Slopsquatting warning&lt;/strong&gt; — when AI says &lt;code&gt;npm install some-plausible-package&lt;/code&gt;, check npmjs.com first. About &lt;strong&gt;20% of AI-generated code references nonexistent packages&lt;/strong&gt;. Attackers register those names with malicious payloads, and you install them instantly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What could have happened at FeedMission
&lt;/h2&gt;

&lt;p&gt;From the 7 above, FeedMission had #2, #3, #6, plus a few app-specific issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slack webhook URL&lt;/strong&gt; rode on ProjectContext into the frontend bundle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsubscribe API&lt;/strong&gt; took just an email address. Anyone's email → instant unsubscribe. Switched to an &lt;code&gt;unsubscribeToken&lt;/code&gt; flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/api/feedback/mine&lt;/code&gt;&lt;/strong&gt; returned the full admin reply text. Now &lt;code&gt;hasReply: boolean&lt;/code&gt; only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team member auth checks&lt;/strong&gt; missing across several APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt; wasn't in &lt;code&gt;.vercelignore&lt;/code&gt; — almost shipped via symlink in a Vercel build.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All fixed in one commit (&lt;code&gt;52efb89&lt;/code&gt;). None of these are "too edge-case to happen to me."&lt;/p&gt;

&lt;h2&gt;
  
  
  My 10-minute pre-deploy routine
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Three grep lines — 5 seconds&lt;/span&gt;
&lt;span class="c"&gt;# Unfinished security code&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"TODO&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;FIXME&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;implement.*later&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;add.*auth"&lt;/span&gt; ./src

&lt;span class="c"&gt;# Hardcoded secrets&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"sk_&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;api_key&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*="&lt;/span&gt; ./src

&lt;span class="c"&gt;# Client-exposed env vars&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rE&lt;/span&gt; &lt;span class="s2"&gt;"NEXT_PUBLIC_.*(SECRET|KEY|TOKEN)"&lt;/span&gt; ./src

&lt;span class="c"&gt;# 2. SQL interpolation and CORS wildcards&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;SELECT&lt;/span&gt;&lt;span class="se"&gt;\|\`&lt;/span&gt;&lt;span class="s2"&gt;INSERT&lt;/span&gt;&lt;span class="se"&gt;\|\`&lt;/span&gt;&lt;span class="s2"&gt;UPDATE&lt;/span&gt;&lt;span class="se"&gt;\|\`&lt;/span&gt;&lt;span class="s2"&gt;DELETE"&lt;/span&gt; ./src
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"Allow-Origin.*&lt;/span&gt;&lt;span class="se"&gt;\*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; ./src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all pass, paste the generated code back to the AI and ask: &lt;em&gt;"Review this code against OWASP Top 10 for vulnerabilities."&lt;/em&gt; Imperfect but a fine first-pass filter.&lt;/p&gt;

&lt;p&gt;GitHub side, turn on three things: &lt;strong&gt;Secret Scanning, Push Protection, CodeQL Code Scanning&lt;/strong&gt;. Plus Dependabot/npm audit in CI for package vulns.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;My prompt tail (every code-generation request):&lt;/strong&gt; &lt;em&gt;"Include auth middleware; read secrets only from process.env and use NEXT_PUBLIC only for public values; always validate user input; no raw SQL; ship complete code without TODO/FIXME."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bonus — Using Supabase? RLS is its own chapter
&lt;/h2&gt;

&lt;p&gt;Next.js + Supabase is the default vibe-coder stack, so RLS gets a dedicated section. RLS (Row Level Security) is PostgreSQL's row-level access control. &lt;em&gt;"This row is readable only by the user whose user_id matches"&lt;/em&gt; — enforced at the database layer.&lt;/p&gt;

&lt;p&gt;Why this matters: &lt;strong&gt;when you create a table in Supabase Studio, RLS is OFF by default.&lt;/strong&gt; Ship &lt;code&gt;NEXT_PUBLIC_SUPABASE_ANON_KEY&lt;/code&gt; to the client in that state and anyone with that key can read or write every row in every table. The anon key effectively becomes a service-role key. Whatever assurance "client-side anon key is safe" gave you, it's gone.&lt;/p&gt;

&lt;p&gt;Turning RLS on isn't enough either. &lt;strong&gt;Without policies, every access is denied.&lt;/strong&gt; You write separate policies per action: &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;. The most frequent mistake is writing &lt;code&gt;USING&lt;/code&gt; (the read/delete-time filter) but forgetting &lt;code&gt;WITH CHECK&lt;/code&gt; (the post-write validation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ✗ Risky — USING only&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"own rows"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- WITH CHECK forgotten!&lt;/span&gt;

&lt;span class="c1"&gt;-- ✓ Safe — both&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="nv"&gt;"own rows"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without WITH CHECK, user_a can INSERT or UPDATE rows claiming user_b's user_id — planting rows or hijacking existing ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three review queries to save in your Supabase SQL Editor:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Tables with RLS still off&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rowsecurity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. RLS on but no policies — everything is rejected&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_tables&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tablename&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policyname&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. INSERT/UPDATE policies missing WITH CHECK&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policyname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qual&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_policies&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'INSERT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UPDATE'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;with_check&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run these after every migration. Empty results on all three = you're clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Top-4 BaaS-specific mistakes:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;RLS off&lt;/strong&gt; — anon key becomes a master key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing WITH CHECK&lt;/strong&gt; — attackers plant rows under someone else's user_id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;service_role key shipped to client&lt;/strong&gt; — &lt;code&gt;SUPABASE_SERVICE_ROLE_KEY&lt;/code&gt; must never be &lt;code&gt;NEXT_PUBLIC&lt;/code&gt;. Server routes / Edge Functions only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissive anon-role policies&lt;/strong&gt; — &lt;code&gt;auth.uid() = user_id&lt;/code&gt; missing means unauthenticated callers reach every row.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same principle applies to Firebase Security Rules, Appwrite Permissions, PocketBase Collection rules: &lt;strong&gt;if the client talks to the database directly, the database is the last line of defense.&lt;/strong&gt; Leave that line empty and no upstream security matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Vibe coding didn't make security worse. The habit of deploying without review did. AI raised the speed. Raise your review speed with it. Three grep lines, one AI review, three GitHub settings, the RLS check if you're on Supabase. Ten minutes.&lt;/p&gt;

&lt;p&gt;Skip those ten minutes and "1.5M API keys leaked" stops being someone else's story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-project-top-ten/" rel="noopener noreferrer"&gt;OWASP Top 10&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://checkmarx.com/blog/security-in-vibe-coding/" rel="noopener noreferrer"&gt;Checkmarx — Security in Vibe Coding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://labs.cloudsecurityalliance.org/research/csa-research-note-ai-generated-code-vulnerability-surge-2026/" rel="noopener noreferrer"&gt;Cloud Security Alliance — AI-Generated CVE Surge 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase RLS docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/code-security/secret-scanning/introduction/about-secret-scanning" rel="noopener noreferrer"&gt;GitHub Secret Scanning docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-vibecoding-security-checklist-for-beginners-ep18" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;. Lazy Developer EP.18.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>webdev</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Upgraded to Tailwind v4 — Config Files Are Gone</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:23:23 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/upgraded-to-tailwind-v4-config-files-are-gone-1o09</link>
      <guid>https://forem.com/lazydev_oh/upgraded-to-tailwind-v4-config-files-are-gone-1o09</guid>
      <description>&lt;p&gt;Tailwind CSS v4 shipped in January 2025 and &lt;code&gt;tailwind.config.js&lt;/code&gt; is gone. Configuration now lives inside the CSS file itself. I migrated a Next.js project — unfamiliar at first, but simpler once you're through it.&lt;/p&gt;

&lt;p&gt;The actual transition is faster than expected. &lt;strong&gt;The official CLI handles about 80% of it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tailwind.config.js&lt;/code&gt; → replaced by a CSS &lt;code&gt;@theme&lt;/code&gt; block&lt;/li&gt;
&lt;li&gt;Rust-based &lt;strong&gt;Oxide compiler&lt;/strong&gt; — up to &lt;strong&gt;5x faster&lt;/strong&gt; full builds, up to &lt;strong&gt;100x faster&lt;/strong&gt; incremental&lt;/li&gt;
&lt;li&gt;Automatic content detection — no more manual &lt;code&gt;content&lt;/code&gt; array&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@tailwind base/components/utilities&lt;/code&gt; → single &lt;code&gt;@import "tailwindcss"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Plugins declared in CSS via &lt;code&gt;@plugin "..."&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real-world number from Tailwind's own benchmark: a design system with 15,000 utility classes saw cold builds drop from 840ms to 170ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config Moved into CSS
&lt;/h2&gt;

&lt;p&gt;v3 kept everything in JS. v4 does it all in one CSS file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* v4 — configure directly in CSS */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--breakpoint-3xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1920px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;68%&lt;/span&gt; &lt;span class="m"&gt;0.19&lt;/span&gt; &lt;span class="m"&gt;245&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter Variable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@theme&lt;/code&gt; uses CSS variables. Design tokens are visible in DevTools at runtime. One less JS dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/theme"&gt;@theme&lt;/a&gt; Naming Convention
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--color-{name}&lt;/code&gt;, &lt;code&gt;--font-{name}&lt;/code&gt;, &lt;code&gt;--spacing-{name}&lt;/code&gt;. Tailwind reads the namespace and generates utility classes automatically. Define &lt;code&gt;--color-brand&lt;/code&gt; and &lt;code&gt;text-brand&lt;/code&gt;, &lt;code&gt;bg-brand&lt;/code&gt;, &lt;code&gt;border-brand&lt;/code&gt; light up immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Oxide Compiler
&lt;/h2&gt;

&lt;p&gt;Rust, not Node. Replaces the old PostCSS plugin. Content path detection is automatic — no more &lt;code&gt;content: ['./src/**/*.tsx']&lt;/code&gt;. Oxide ships inside the &lt;code&gt;tailwindcss&lt;/code&gt; v4 package, no separate install. Integrates with Vite and PostCSS pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A — one command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @tailwindcss/upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handles config conversion and class renames for projects without custom plugins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B — manual (Next.js / PostCSS)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;tailwindcss@latest @tailwindcss/postcss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// postcss.config.js (v4)&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tailwindcss/postcss&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* globals.css (v4) */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6366f1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tailwind.config.js&lt;/code&gt; can be deleted or kept — v4 doesn't read it. Deleting it is cleaner for team repos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins Now Live in CSS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"@tailwindcss/typography"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"@tailwindcss/forms"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"./plugins/my-plugin.js"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6366f1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;plugins&lt;/code&gt; array in &lt;code&gt;tailwind.config.js&lt;/code&gt; is gone. Pass a package name or a file path to &lt;code&gt;@plugin&lt;/code&gt; and it works. Existing &lt;code&gt;addUtilities&lt;/code&gt; and &lt;code&gt;addComponents&lt;/code&gt; APIs mostly still apply, but parts of the plugin API changed — verify behavior after migrating.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;outline-none&lt;/code&gt; Gotcha
&lt;/h2&gt;

&lt;p&gt;v3: &lt;code&gt;outline-none&lt;/code&gt; rendered as &lt;code&gt;outline: 2px solid transparent&lt;/code&gt; — still accessible.&lt;br&gt;
v4: &lt;code&gt;outline-none&lt;/code&gt; renders as &lt;code&gt;outline: none&lt;/code&gt; — actually removes the outline.&lt;/p&gt;

&lt;p&gt;If you used &lt;code&gt;outline-none&lt;/code&gt; to hide focus rings on buttons or inputs, swap in &lt;code&gt;outline-hidden&lt;/code&gt;. Expect this to surface during accessibility checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  v3 vs v4 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;Area&lt;/th&gt;
&lt;th&gt;v3&lt;/th&gt;
&lt;th&gt;v4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tailwind.config.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSS &lt;code&gt;@theme&lt;/code&gt; block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import&lt;/td&gt;
&lt;td&gt;three &lt;code&gt;@tailwind&lt;/code&gt; lines&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@import "tailwindcss"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content detection&lt;/td&gt;
&lt;td&gt;manual array&lt;/td&gt;
&lt;td&gt;automatic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiler&lt;/td&gt;
&lt;td&gt;PostCSS (Node)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Oxide (Rust)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugins&lt;/td&gt;
&lt;td&gt;&lt;code&gt;plugins: [...]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@plugin "..."&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outline-none&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;transparent outline&lt;/td&gt;
&lt;td&gt;actual &lt;code&gt;none&lt;/code&gt; (use &lt;code&gt;outline-hidden&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Should You Upgrade Now?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;New project&lt;/strong&gt; → v4. No reason not to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Existing v3 project&lt;/strong&gt; → no rush. v3 is still supported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy custom-plugin stack&lt;/strong&gt; → stay on v3 until you've tested each plugin against the v4 API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build times biting&lt;/strong&gt; → v4 is worth the migration cost just for the Oxide numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q. Do I need to delete &lt;code&gt;tailwind.config.js&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
No — v4 doesn't read it. The upgrade CLI handles conversion. Delete for cleanliness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Separate Oxide install?&lt;/strong&gt;&lt;br&gt;
No. Included in the &lt;code&gt;tailwindcss&lt;/code&gt; v4 package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. How long does migration take?&lt;/strong&gt;&lt;br&gt;
Small Next.js projects: 30 minutes including manual review. Larger ones with custom plugins and dynamic class composition (&lt;code&gt;bg-${color}-500&lt;/code&gt; patterns): a couple hours, because those aren't auto-migrated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/blog/tailwindcss-v4-alpha" rel="noopener noreferrer"&gt;Open-sourcing progress on Tailwind CSS v4.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tailwindcss.com/blog/tailwindcss-v4" rel="noopener noreferrer"&gt;Tailwind CSS v4.0 release post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-tailwind-css-v4-migration-guide-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Gemma 4 vs Llama 4 vs Mistral Small 4: The 2026 Open-Source LLM Picks</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Mon, 13 Apr 2026 02:23:22 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/gemma-4-vs-llama-4-vs-mistral-small-4-the-2026-open-source-llm-picks-20e7</link>
      <guid>https://forem.com/lazydev_oh/gemma-4-vs-llama-4-vs-mistral-small-4-the-2026-open-source-llm-picks-20e7</guid>
      <description>&lt;p&gt;Three heavyweights dropped this year: Gemma 4 (Google), Llama 4 (Meta), Mistral Small 4 (Mistral). All free to run. All structurally different. Here's which one fits which job.&lt;/p&gt;

&lt;p&gt;Short answer: long context → &lt;strong&gt;Llama 4 Scout&lt;/strong&gt;. License-clean commercial use → &lt;strong&gt;Mistral Small 4&lt;/strong&gt;. On-device → &lt;strong&gt;Gemma 4 E2B / E4B&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Take
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gemma 4 (31B / 26B MoE)&lt;/th&gt;
&lt;th&gt;Llama 4 Scout&lt;/th&gt;
&lt;th&gt;Mistral Small 4&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Architecture&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dense (31B) · MoE (26B/A4B)&lt;/td&gt;
&lt;td&gt;MoE (17B active / 109B)&lt;/td&gt;
&lt;td&gt;MoE (~22B active / 119B)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;E2B/E4B 128K · 31B/26B 256K&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;256K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Gemma ToU&lt;/td&gt;
&lt;td&gt;Llama 4 Community&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Apache 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multimodal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;text + image + video + OCR (E2B/E4B add &lt;strong&gt;audio&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;text + image (early fusion)&lt;/td&gt;
&lt;td&gt;text + image (first in Small series)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Edge fit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Excellent (E2B/E4B)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low (multi-GPU even quantized)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  MoE vs Dense
&lt;/h2&gt;

&lt;p&gt;MoE is a bank of specialized tellers — only the relevant experts fire per input. Llama 4 Scout: 109B total, 17B active. Mistral Small 4: 119B total across 128 experts, ~22B active. Gemma 4 26B: the "small MoE" path — 26B total, ~3.8B active, targeting 4B-speed with bigger-model intelligence.&lt;/p&gt;

&lt;p&gt;Gemma 4 E2B, E4B, and 31B are Dense. Every parameter fires on every token. Higher compute per parameter, but memory requirements scale linearly and planning is easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One MoE trap people hit:&lt;/strong&gt; inference compute drops, but all weights still need to sit in memory. Llama 4 Scout in fp16 = ~218GB VRAM. 4-bit = ~55GB. "Only 17B active so it's lightweight" is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context Window — 10M, 256K, 128K
&lt;/h2&gt;

&lt;p&gt;Llama 4 Scout's 10M is the outlier. Meta got there via &lt;strong&gt;iRoPE&lt;/strong&gt; — interleaved RoPE that holds accuracy past the training sequence length. Practical impact: you can drop an entire monorepo into one prompt and skip the RAG pipeline altogether.&lt;/p&gt;

&lt;p&gt;Mistral Small 4 sits at 256K. Gemma 4's small variants (E2B/E4B) are 128K; the medium 31B and 26B MoE jump to 256K. For normal-scale work — books, research paper batches, long meeting transcripts — 128K is already more than enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Maverick&lt;/strong&gt; on SWE-bench: 76.8 to 80.8 depending on the evaluation variant. Open-source top tier — but not "absolute #1." GLM-5 (77.8) shows up right next to it on SWE-bench Verified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Scout&lt;/strong&gt; is smaller than Maverick but wins on repo-scale analysis thanks to 10M context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 31B&lt;/strong&gt; shines on multimodal tasks relative to its size class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistral Small 4&lt;/strong&gt; (per Mistral's evals) matches or surpasses GPT-OSS 120B and Qwen-class models on several key benchmarks — at ~22B active.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benchmarks and day-to-day use diverge. Run them yourself before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multimodal — Images, Video, Audio
&lt;/h2&gt;

&lt;p&gt;None of these three is text-only in 2026.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4&lt;/strong&gt; is natively multimodal across every variant: text, image, video, OCR. E2B and E4B add &lt;strong&gt;native audio input&lt;/strong&gt; — voice assistants and on-device transcription become direct use cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Scout/Maverick&lt;/strong&gt; use early fusion — text and vision tokens unified inside the foundation model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mistral Small 4&lt;/strong&gt; is the first in the Mistral Small series to support native vision. Images ride in the normal API message array alongside text, inside the same 256K window.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Licenses (Actually Read Before Shipping)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mistral Small 4 / Apache 2.0&lt;/strong&gt; — zero restrictions. Fine-tune, redistribute, embed in SaaS, ship it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Llama 4 Community&lt;/strong&gt; — commercial use fine below 700M MAU, but Meta's approval is required above that (sole discretion). Also: mandatory &lt;strong&gt;"Built with Llama"&lt;/strong&gt; badge on a related web or in-app page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 / Google Gemma ToU&lt;/strong&gt; — you can't use Gemma outputs to train competing LLMs, and AI-adjacent services need to read the clauses carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Edge Deployment Reality
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;fp16 VRAM&lt;/th&gt;
&lt;th&gt;4-bit VRAM&lt;/th&gt;
&lt;th&gt;Realistic hardware&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E4B&lt;/td&gt;
&lt;td&gt;~8GB&lt;/td&gt;
&lt;td&gt;~3GB&lt;/td&gt;
&lt;td&gt;Laptop / phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;~62GB&lt;/td&gt;
&lt;td&gt;~16GB&lt;/td&gt;
&lt;td&gt;RTX 4090 / M2 Max&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 4 Scout&lt;/td&gt;
&lt;td&gt;~218GB&lt;/td&gt;
&lt;td&gt;~55GB&lt;/td&gt;
&lt;td&gt;Multi-GPU / H100 at Int4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral Small 4&lt;/td&gt;
&lt;td&gt;~238GB&lt;/td&gt;
&lt;td&gt;~60GB&lt;/td&gt;
&lt;td&gt;Multi-GPU / high-end workstation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Gemma 4 E4B at 4-bit = ~3GB. Runs on a laptop. For smartphone deployments E2B is the target. Llama 4 Scout and Mistral Small 4 stay in server territory even quantized — the full MoE weights have to fit in memory regardless of active count.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Combine All Three
&lt;/h2&gt;

&lt;p&gt;Routing by request type is more realistic than picking one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request type                    → model
-------------------------------------------
whole-doc / whole-repo analysis → Llama 4 Scout (10M context)
image + video + audio input     → Gemma 4
commercial API traffic          → Mistral Small 4 (Apache 2.0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using hosted APIs (Together AI, Groq, Fireworks) on top of this routing lets you optimize both cost and capability together.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q. How does Scout actually handle 10M tokens?&lt;/strong&gt;&lt;br&gt;
iRoPE — Meta's interleaved version of RoPE position encoding. Extends accuracy well past training length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Which is most commercial-friendly?&lt;/strong&gt;&lt;br&gt;
Mistral Small 4. Apache 2.0. No MAU cap, no branding requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Is MoE always better than Dense?&lt;/strong&gt;&lt;br&gt;
No. Inference compute drops, but memory scales with total parameters. Edge = Dense small or compact MoE like Gemma 4 26B. MoE only pays off with multi-GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Best at coding?&lt;/strong&gt;&lt;br&gt;
Llama 4 Maverick (76.8–80.8 on SWE-bench) — top tier, not #1. GLM-5 (77.8) is right there too. Mistral Small 4 is fine for general code review; Scout's 10M wins whole-repo work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/blog/gemma4" rel="noopener noreferrer"&gt;Hugging Face — Welcome Gemma 4&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ai.meta.com/blog/llama-4-multimodal-intelligence/" rel="noopener noreferrer"&gt;Meta AI — The Llama 4 herd&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.llama.com/llama4/license/" rel="noopener noreferrer"&gt;Llama 4 Community License&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mistral.ai/news/mistral-small-4" rel="noopener noreferrer"&gt;Mistral Small 4 announcement&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-gemma-4-vs-llama-4-vs-mistral-small-4-llm-comparison-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;. Always read each model's official license before commercial deployment — this post is not legal advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I Engineered How AI Works for Me — My Claude Code Harness Setup</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Sun, 12 Apr 2026 17:30:46 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/i-engineered-how-ai-works-for-me-my-claude-code-harness-setup-5a50</link>
      <guid>https://forem.com/lazydev_oh/i-engineered-how-ai-works-for-me-my-claude-code-harness-setup-5a50</guid>
      <description>&lt;h2&gt;
  
  
  What Is Harness Engineering
&lt;/h2&gt;

&lt;p&gt;A harness is originally the gear you put on a horse. Here it means the work framework you put on AI. It's not just writing better prompts — it's building a system that defines how Claude works.&lt;/p&gt;

&lt;p&gt;There are three components. &lt;strong&gt;Rules&lt;/strong&gt; is the CLAUDE.md at the project root — the rules Claude must always follow in this project. &lt;strong&gt;Commands&lt;/strong&gt; are saved files for repeated task requests: things like /workflow and /plan-and-spec. &lt;strong&gt;Hooks&lt;/strong&gt; are logic that runs automatically before or after specific actions. This is the most powerful layer of the three.&lt;/p&gt;

&lt;p&gt;Rules define "what to do." Commands define "how to do it." Hooks enforce "what must never happen." The three layers combine into a single framework.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Background on This Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Commands and Hooks concepts exist in Anthropic's official Claude Code documentation. Harness engineering is a method of combining these features to automate an entire personal development workflow. It uses official features — not hidden ones.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Full Structure at a Glance
&lt;/h2&gt;

&lt;p&gt;The file structure is the fastest way to understand it. It's split into a global area and a per-project area. The global area is shared across every project on your machine. Set it up once and it carries over to every project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/                        ← shared across machine (set up once)
  commands/
    init-harness.md               ← auto-build harness (this file)
    new-project.md                ← idea → project setup
    session-resume.md             ← restore context on session resume
  hooks/
    block-dangerous.sh            ← block dangerous commands
  settings.json                  ← hook wiring

project/                          ← auto-generated per project
  CLAUDE.md                      ← project rules
  progress.md                    ← current task state
  dev-log.md                     ← feature-by-feature dev log
  docs/                          ← store design documents
  screenshots/                   ← store screenshots
  .claude/commands/
    workflow.md                    ← full pipeline orchestrator
    plan-and-spec.md               ← design + fact-check (Planner)
    tdd.md                         ← implementation (Generator)
    ui-ux.md                       ← UI/UX research
    verify.md                      ← verification (Evaluator)
    commit.md                      ← commit + merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fat01tgypyzvkld7bnlc1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fat01tgypyzvkld7bnlc1.png" alt="Full harness structure — global + project two-layer architecture" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key is separation. What can be used in any project goes global; what is only meaningful in this project goes inside the project. init-harness automatically creates the entire project area.&lt;/p&gt;

&lt;h2&gt;
  
  
  init-harness: From Analysis to Generation
&lt;/h2&gt;

&lt;p&gt;Whether starting a new project or attaching a harness to an existing one, you just run /init-harness. It proceeds automatically through five steps. No confirmation prompts in between. It shows one analysis summary, then moves straight to generation.&lt;/p&gt;

&lt;p&gt;Step 1 is project analysis. It reads package.json to identify the stack, checks the folder structure 3 levels deep, reads git log for commit patterns, and checks .env.example for integrated services. It only reads. It changes nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: project analysis (read only)&lt;/span&gt;

git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;     &lt;span class="c"&gt;# identify commit patterns&lt;/span&gt;
git branch &lt;span class="nt"&gt;-a&lt;/span&gt;             &lt;span class="c"&gt;# check branch strategy&lt;/span&gt;

&lt;span class="c"&gt;# output format after analysis&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;project analysis]
- stack:           Next.js 15 / TypeScript / Supabase
- structure:       App Router, no src/, feature-based folders
- commit pattern:  feat/fix/refactor prefix
- branch strategy: feature/&lt;span class="k"&gt;*&lt;/span&gt; → direct merge to main
- integrations:    Supabase, Resend, LemonSqueezy
- commands to generate: workflow, plan-and-spec, tdd, ui-ux, verify, commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2 is CLAUDE.md generation. It fills in the tech stack, architecture rules, and absolute prohibitions from the analysis results. It's not a blank template — it's a file built from actually reading the project. Items like "i18n keys only for multilingual text," "no hardcoding," and "no force push to main" are inserted automatically.&lt;/p&gt;

&lt;p&gt;Step 3 is command file generation. workflow, plan-and-spec, tdd, ui-ux, verify, and commit are created inside .claude/commands/. Step 4 is generating the project base files: progress.md, dev-log.md, docs/, and screenshots/. Step 5 is checking global files. If session-resume, new-project, or block-dangerous.sh don't exist, it creates them; if they already exist, it leaves them alone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5rxs537cfdq6t9kxao0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5rxs537cfdq6t9kxao0.png" alt="/workflow pipeline — branch → design → implement → verify → commit 5 steps" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Command Pipeline
&lt;/h2&gt;

&lt;p&gt;workflow.md is the orchestrator for the entire pipeline. Running /workflow "feature name" calls the rest in order. Each step automatically moves to the next when complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /workflow "feedback auto-classify feature"&lt;/span&gt;

0. assess current state    &lt;span class="c"&gt;# progress.md + git log --oneline -10&lt;/span&gt;
1. branch setup            &lt;span class="c"&gt;# feature/feedback-auto-classify&lt;/span&gt;
2. design + fact-check     &lt;span class="c"&gt;# run /plan-and-spec → Planner&lt;/span&gt;
3. implement               &lt;span class="c"&gt;# run /tdd → Generator&lt;/span&gt;
4. verify                  &lt;span class="c"&gt;# run /verify → Evaluator&lt;/span&gt;
5. commit + merge          &lt;span class="c"&gt;# run /commit&lt;/span&gt;

&lt;span class="c"&gt;# if verification fails → go back to step 3, fix, then re-verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;plan-and-spec forces a web-search fact-check before implementation. It first confirms whether the library actually exists and whether the API can actually be implemented. If anything is 100% impossible, it proposes an alternative. The design document is saved to docs/spec-featurename.md.&lt;/p&gt;

&lt;p&gt;tdd breaks things into feature units and implements them one at a time. It includes a step where mock data is injected into each completed screen to make it look real. This bakes in the pattern from EP.08 where I took a screenshot every time I finished a platform-specific widget.&lt;/p&gt;

&lt;p&gt;verify is the step that compares the design document against the actual implementation. It checks for missing features, untranslated strings, and build errors. If there are failures, it outputs the fix method alongside them and returns to tdd. It's not just "does it run" — it's "was it built as designed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Planner → Generator → Evaluator
&lt;/h2&gt;

&lt;p&gt;The core of the command pipeline is the three-role separation. A single Claude switches roles as it works. plan-and-spec takes the Planner role, tdd takes Generator, verify takes Evaluator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Planner&lt;/strong&gt; designs and fact-checks before implementation. It first verifies "is this even possible." It web-searches to confirm the library actually exists, checks similar implementation examples, and reviews API constraints — then produces a design document. This step exists to prevent the situation where you start implementing without fact-checking and get stuck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generator&lt;/strong&gt; implements feature by feature while reading the design document. If the design changes during implementation, it immediately updates docs/spec-*.md and states the reason. Keeping design and code in sync is the key. There is no "I'll update it later."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluator&lt;/strong&gt; compares the design document against the actual implementation. It checks for missing features, untranslated strings, and missing error handling. The Generator does not self-verify. By separating roles, things the implementor is likely to miss get caught by different eyes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What Actually Happened With the EP.05 Clustering Feature&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When building the automatic feedback clustering, the Planner fact-checked the actual parameters for pgvector and the Voyage AI embedding API. The Generator implemented clustering.ts — 188 lines — feature by feature. The Evaluator compared it against the design document and caught a missing cosine similarity threshold handler. Before this, that kind of omission was the type I only caught after shipping.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Hooks Enforce
&lt;/h2&gt;

&lt;p&gt;Commands are optional, but hooks run automatically. Before Claude executes any bash command, block-dangerous.sh runs first. If it returns exit 2, the command is blocked. It blocks two things.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# block direct force push to main&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"push.*--force.*main&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;push.*main.*--force"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"main force push blocked"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# block .env commit&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"git add.*&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;env&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env file commit blocked"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is wired up as a PreToolUse hook in settings.json. It's automatically called before Claude executes the Bash tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;~/.claude/settings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash ~/.claude/hooks/block-dangerous.sh"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantage of hooks is that they block mistakes at the source. Even if a force push to main slips in by accident, the system stops it. It's not a human checking — it's the system blocking. Having just these two in place defends against the most catastrophic mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Automation Mode
&lt;/h2&gt;

&lt;p&gt;With the harness set up, running Claude Code with the --dangerously-skip-permissions flag lets it run to completion on its own without asking for confirmation. No "is it okay to do this" in the middle. Run /workflow and everything from branch creation to commit and merge happens automatically.&lt;/p&gt;

&lt;p&gt;This mode is not the dangerous part. Using it without hooks is dangerous. The hooks block main force push and .env commits, so the two critical mistakes are automatically defended against. Set up the harness first, then use it — that's the order.&lt;/p&gt;

&lt;p&gt;When a session drops and resumes, I use /session-resume. It reads the last 30 lines of progress.md and dev-log.md, plus git log and git status, then summarizes "how far we got and what's next" before picking right back up. It pairs with the CLAUDE.md automation from EP.01. One maintains the rules; the other restores the state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;A few things changed after building the harness. In numbers, it looks like this.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starting a new project&lt;/td&gt;
&lt;td&gt;explaining rules every time (10–15 min)&lt;/td&gt;
&lt;td&gt;/init-harness, one line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature dev order&lt;/td&gt;
&lt;td&gt;repeating the order every time&lt;/td&gt;
&lt;td&gt;/workflow "feature name"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session resume&lt;/td&gt;
&lt;td&gt;re-explaining context&lt;/td&gt;
&lt;td&gt;/session-resume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI dev research&lt;/td&gt;
&lt;td&gt;requesting separately each time&lt;/td&gt;
&lt;td&gt;built into ui-ux.md&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design docs&lt;/td&gt;
&lt;td&gt;forgotten, not written&lt;/td&gt;
&lt;td&gt;auto-generated + auto-updated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There was an unexpected result. I built this system because I was lazy, but I ended up working more carefully. I started writing design documents. I started doing fact-checks. The steps I used to skip because they were annoying are now baked into the commands — so they just happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Q. If I run init-harness, do I not need to write CLAUDE.md myself?
&lt;/h3&gt;

&lt;p&gt;It auto-generates one, but it doesn't fully replace writing it yourself. What init-harness creates is a draft based on the project analysis. Team conventions, specific library constraints, and deployment environment details still need to be added manually. Use it as: auto-generate, then fill in the gaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Do I have to use commands like /workflow every time?
&lt;/h3&gt;

&lt;p&gt;No. Simple bug fixes or small changes can just be asked in plain language. Commands are only for feature development that needs to go all the way from start to finish properly. Only hooks are always running in the background — everything else is optional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Isn't the --dangerously-skip-permissions flag dangerous?
&lt;/h3&gt;

&lt;p&gt;It's dangerous without hooks. The hooks block main force push and .env commits, so the order is: set up the harness first, then use it. It's not the flag that's dangerous — using it without hooks is. With just these two in place, full automation mode is usable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Can this structure be used for team projects?
&lt;/h3&gt;

&lt;p&gt;This structure was designed for solo development. To use it on a team, you'd need to modify the direct-to-main merge section and the branch strategy. Update CLAUDE.md to match team conventions and add a PR creation step to commit.md. The structure itself can be adapted for teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2esw8fz33mrde9knxtz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2esw8fz33mrde9knxtz.png" alt="Harness Before/After comparison table" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Setting up this structure took a day or two. Writing command files, making hooks, testing the flow. At first I wasn't sure it was right. Now, every time I start a new project, /init-harness handles everything — so that time wasn't wasted.&lt;/p&gt;

&lt;p&gt;The harness is never finished. As you use it, the gaps become visible, and you update the command files each time. It's not about building a perfect framework — it's about cutting out annoying things one by one. That's the Lazy Developer way.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-claude-code-harness-engineering-setup-ep17" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Harness Engineering — The Environment Matters More Than the Prompt</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Sun, 12 Apr 2026 16:37:09 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/harness-engineering-the-environment-matters-more-than-the-prompt-3cod</link>
      <guid>https://forem.com/lazydev_oh/harness-engineering-the-environment-matters-more-than-the-prompt-3cod</guid>
      <description>&lt;h2&gt;
  
  
  What Is a Harness
&lt;/h2&gt;

&lt;p&gt;The word comes from a horse harness. The harness is the entire apparatus that guides the horse called AI in the desired direction. It's the concept of designing not the model itself, but the entire environment in which that model works. It's not about a single prompt line — it's about setting up the entire board.&lt;/p&gt;

&lt;p&gt;Context files, skill files, MCP servers, and execution loops are all included. Verification processes and human intervention points are all part of the harness. Humans are part of the harness too. It is not a system where AI runs alone.&lt;/p&gt;

&lt;p&gt;It's not about what to ask AI. It's about how to set up the board where AI can work. The core argument is that the environment is the bottleneck, not the model. That's why results can differ 10x with the same model depending on environment setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Limits of Prompt Engineering
&lt;/h2&gt;

&lt;p&gt;Once, the single phrase "think step by step" (Chain-of-Thought) boosted math accuracy by 39%p (Wei et al. 2022, GSM8K benchmark). A 2025 Wharton GAIL study remeasured with the latest models and found it dropped to about 3%p. Evidence that as models advance, the effect of prompt tricks is disappearing.&lt;/p&gt;

&lt;p&gt;The more advanced the model, the more built-in reasoning it already has. There is less room for prompt manipulation. No matter how sophisticated a system prompt is, there are fundamental limits. Designing structure yields better long-term returns than spending time hunting for tricks.&lt;/p&gt;

&lt;p&gt;A harness, by contrast, can be reused even as models are upgraded. The structure itself is not tied to a specific model. A CLAUDE.md written this year will work the same with next year's models. This is why investing in harness rather than prompts is the right move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three-Stage Evolution: Prompt → Context → Harness
&lt;/h2&gt;

&lt;p&gt;There are three stages. Prompt engineering → context engineering → harness engineering. Each maps to "What to ask → What to show → How to design." We are now at the transition into the third stage.&lt;/p&gt;

&lt;p&gt;Context engineering is deciding what ingredients to give AI. Harness is designing the entire kitchen that holds those ingredients. Good ingredients alone are not enough. Tool placement and task sequence must be designed together.&lt;/p&gt;

&lt;p&gt;There is a case from the medical field that demonstrates this difference. Providing relevant data and restricting scope reduced hallucinations from 40% to 0%. Without changing the model. Changing the context alone changed the results. Harness is a broader concept that includes this context design.&lt;/p&gt;

&lt;h2&gt;
  
  
  5 Core Components
&lt;/h2&gt;

&lt;p&gt;The 5 core elements of a harness are Agent.md (CLAUDE.md), Skills, MCP, Hooks, and Sub-agents. Each has a different role and they are used in combination. What makes the difference is not which tool you use, but how well you design these 5 structures.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Element&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;th&gt;Key Caution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent.md&lt;/td&gt;
&lt;td&gt;Documenting project rules&lt;/td&gt;
&lt;td&gt;New employee onboarding manual&lt;/td&gt;
&lt;td&gt;Keep under 300 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skills&lt;/td&gt;
&lt;td&gt;Separating task-specific expertise&lt;/td&gt;
&lt;td&gt;Department-specific reference files&lt;/td&gt;
&lt;td&gt;Separate file per task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;td&gt;Connecting external tools&lt;/td&gt;
&lt;td&gt;USB hub&lt;/td&gt;
&lt;td&gt;Connect only what's needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hooks&lt;/td&gt;
&lt;td&gt;Mandatory execution rules&lt;/td&gt;
&lt;td&gt;Automatic checklist&lt;/td&gt;
&lt;td&gt;Use instead of prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub-agents&lt;/td&gt;
&lt;td&gt;Parallel task processing&lt;/td&gt;
&lt;td&gt;Team member division of labor&lt;/td&gt;
&lt;td&gt;Verify dependencies first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftii4wwy2e44auki7qr0x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftii4wwy2e44auki7qr0x.png" alt="Harness Engineering 5 Core Elements — Agent.md, Skills, MCP, Hooks, Sub-Agent" width="800" height="301"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This structure can be applied equally in both Claude Code and Cursor. The harness structure works the same regardless of the tool. Whether you have this structure matters more than which AI coding tool you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Write CLAUDE.md Properly
&lt;/h2&gt;

&lt;p&gt;CLAUDE.md (or Agent.md) is the foundation file of the harness. It documents project structure, coding rules, and standards AI must follow. The general consensus is under 300 lines. HumanLayer stated they keep it under 60 lines in their own projects. An ETH Zurich study found that auto-generated CLAUDE.md by LLMs actually increased token costs by 20% and lowered performance. Writing it yourself is better.&lt;/p&gt;

&lt;p&gt;The key is not writing things that Claude can figure out by reading the code. Standard language conventions, per-file descriptions — these are left out. Conversely, things that cannot be known from code alone must be written. Bash commands, branch rules, project-specific architecture decisions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What to Write&lt;/th&gt;
&lt;th&gt;What to Skip&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bash commands Claude cannot infer&lt;/td&gt;
&lt;td&gt;Things derivable by reading the code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code style rules that differ from defaults&lt;/td&gt;
&lt;td&gt;Standard language conventions (Claude already knows)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test runner / how to run tests&lt;/td&gt;
&lt;td&gt;Detailed API docs (just link them)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch naming, PR rules&lt;/td&gt;
&lt;td&gt;Frequently changing information&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment quirks, required env vars&lt;/td&gt;
&lt;td&gt;"Write clean code"-style obvious things&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scope of application varies by file location. &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; applies to all projects. &lt;code&gt;CLAUDE.md&lt;/code&gt; at the project root applies only to that project. &lt;code&gt;CLAUDE.local.md&lt;/code&gt; is for personal settings and goes in .gitignore. In a monorepo, placing separate CLAUDE.md files in subdirectories applies them hierarchically.&lt;/p&gt;

&lt;p&gt;Not everything goes into CLAUDE.md. The Progressive Disclosure pattern of referencing separate documents is key. Here's what it looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Code style
- Use ES modules (import/export), not CommonJS (require)
- Destructure imports when possible

# Workflow
- Typecheck when done making code changes
- Run single tests, not the full suite

# References
See @README.md for project overview
Git workflow: @docs/git-instructions.md
Personal overrides: @~/.claude/my-project-instructions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CLAUDE.md Writing Principles&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under 300 lines is the general consensus. Shorter is better. Longer makes AI slower&lt;/li&gt;
&lt;li&gt;Not a manual, but an index. Write only "where to find what"&lt;/li&gt;
&lt;li&gt;Don't write things Claude can figure out by reading the code&lt;/li&gt;
&lt;li&gt;Separate detailed rules into separate documents and reference with &lt;code&gt;@path&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remove stale documents immediately. AI follows outdated rules&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Skill File Design
&lt;/h2&gt;

&lt;p&gt;Skill files are task-specific expertise separated into the &lt;code&gt;.claude/skills/&lt;/code&gt; folder. The difference from CLAUDE.md is "always loaded vs. loaded only when needed." Putting everything in CLAUDE.md wastes the context window. Skills are only loaded when that task is being performed.&lt;/p&gt;

&lt;p&gt;The actual directory structure 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;.claude/skills/
  api-conventions/
    SKILL.md        # main (required)
    reference.md    # detailed docs (loaded on demand)
    examples.md     # usage examples
  fix-issue/
    SKILL.md
  deploy/
    SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; in the SKILL.md frontmatter lets you invoke it as a slash command. Setting &lt;code&gt;disable-model-invocation: true&lt;/code&gt; means it won't be invoked automatically by AI — it only runs when a human calls it directly. There are two types: reference skills and task skills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Reference — API rules (auto-loaded by Claude)&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-conventions&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REST API design conventions&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Use kebab-case for URL paths&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Use camelCase for JSON properties&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Always include pagination for list endpoints&lt;/span&gt;

&lt;span class="c1"&gt;# Task — Fix GitHub issue (/fix-issue 123)&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fix-issue&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Fix a GitHub issue&lt;/span&gt;
&lt;span class="na"&gt;disable-model-invocation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;Analyze and fix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$ARGUMENTS&lt;/span&gt;
&lt;span class="s"&gt;1. Check issue with `gh issue view`&lt;/span&gt;
&lt;span class="s"&gt;2. Search code → fix → test → create PR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a case of applying this to video production automation. Researcher, script, subtitles, voice, scene design, rendering, QA — split into 7 agent skills. Work that took 9 hours was reduced to 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks — Mandatory Rules AI Cannot Ignore
&lt;/h2&gt;

&lt;p&gt;Hooks are mechanisms that force-execute tasks that AI might ignore even when instructed via prompt. Registered in &lt;code&gt;settings.json&lt;/code&gt;, they run automatically when specific events occur. Prompts can be skipped. Hooks cannot. Exit code 0 means allow, 2 means block.&lt;/p&gt;

&lt;p&gt;I documented the 5 most commonly used hooks in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;settings.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;practical&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Hooks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Auto-format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;after&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;edit&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jq -r '.tool_input.file_path' | xargs npx prettier --write"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Block&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dangerous&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(rm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-rf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--hard)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".claude/hooks/pre-bash-firewall.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Auto-verify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;task&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;completion&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Check if all tasks are complete. Continue if incomplete."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;macOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;desktop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;notification&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Notification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osascript -e 'display notification &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Claude Code&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; with title &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Needs attention&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hooks that enforce package managers are also useful. In a pnpm project, if AI tries to use npm, it gets blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash — enforce pnpm Hook&lt;/span&gt;
&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.command // ""'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; pnpm-lock.yaml &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eq&lt;/span&gt; &lt;span class="s1"&gt;'\bnpm\b'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"This repo uses pnpm. Replace npm with pnpm."&lt;/span&gt; 1&amp;gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2  &lt;span class="c"&gt;# block&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0  &lt;span class="c"&gt;# allow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Things to Watch When Connecting MCP
&lt;/h2&gt;

&lt;p&gt;MCP is an adapter that connects AI agents to external tools. Like a USB hub — it attaches external capabilities like Gmail, browser automation, and document search to AI. In practice, it's used to automatically send a report email via Gmail MCP after completing Excel work.&lt;/p&gt;

&lt;p&gt;There is a caveat. Connecting more MCPs is not better. It wastes tokens and creates confusion. The principle is to connect only what's needed. If it feels like MCP is being forced into use, removing it is the right call.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MCP Connection Principles&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect only what's immediately needed&lt;/li&gt;
&lt;li&gt;The more connections, the higher the chance of AI judgment errors&lt;/li&gt;
&lt;li&gt;Disconnect MCPs that are not actually being used&lt;/li&gt;
&lt;li&gt;Excessively connected MCPs waste the context window&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Practical Application — Difference Seen in Before/After
&lt;/h2&gt;

&lt;p&gt;The OpenAI Codex experiment is the representative case. 5 months, 1 million lines of code, 0 lines written directly by humans. 1,500 PRs merged. One engineer completed an average of 3.5 tasks per day. In Terminal Bench 2.0, the same model (Opus 4.6) ranked 40th with the default harness (Claude Code defaults) and 1st with an optimized harness. The harness, not the model, determined the ranking.&lt;/p&gt;

&lt;p&gt;Mitchell Hashimoto (HashiCorp founder) summarized the core principle this way. "Every time the agent makes a mistake, engineer it so that mistake can never happen again." Not fixing mistakes, but building a structure that makes mistakes impossible.&lt;/p&gt;

&lt;p&gt;I summarized specifically how it differs in a Before/After breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before (Prompts Only)&lt;/th&gt;
&lt;th&gt;After (Harness Applied)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Make an email validation function"&lt;/td&gt;
&lt;td&gt;"Write validateEmail. Tests: &lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt; → true, invalid → false. Run tests after implementing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Make the dashboard look nice"&lt;/td&gt;
&lt;td&gt;[Screenshot attached] "Implement this design. Take a screenshot of the result and compare with the original"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Build failed"&lt;/td&gt;
&lt;td&gt;"Failed with this error: [paste error]. Don't suppress the error — fix the root cause"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full test output of 4,000 lines printed&lt;/td&gt;
&lt;td&gt;Hooks silence success, show only failures (Back-Pressure pattern)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Common Anti-Patterns to Avoid&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kitchen Sink Session&lt;/strong&gt; — mixing unrelated tasks in one session → reset context with &lt;code&gt;/clear&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same fix repeated 3 times&lt;/strong&gt; — learning failure → new prompt reflecting the failure cause after &lt;code&gt;/clear&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLAUDE.md exceeding 200 lines&lt;/strong&gt; — context waste → keep only essentials, move the rest to Skills&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endless exploration&lt;/strong&gt; — investigation request without scope → split into sub-agents&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fylja8v1sxds03fh1y6ow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fylja8v1sxds03fh1y6ow.png" alt="Same Model, Different Results — Before/After comparison with Terminal Bench and Codex stats" width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is one common thread. The initial setup takes time. Once built, subsequent iterations automatically accelerate. The setup cost is one-time; the effect is cumulative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Principles in Design
&lt;/h2&gt;

&lt;p&gt;Giving only the final goal is not enough. Intermediate stage verification criteria must also be specified. Not "just produce the output" but "must pass this criteria at this stage to proceed to the next." When goals are vague, AI looks for shortcuts.&lt;/p&gt;

&lt;p&gt;Semi-automatic structures with explicit approval gates are more realistic than fully automatic. In the video production automation case, approval gates were placed at 4 points: script, voice, scene design, and QA. Not AI running to the end alone, but humans checking in between. At the current level of technology, this is the realistic choice.&lt;/p&gt;

&lt;p&gt;Fix code patterns before they go wrong. Lock good rules in with linters and tests. Remove stale rules quickly. If AI follows incorrect past rules, problems accumulate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Design Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Context file (CLAUDE.md etc.): under 300 lines, short table-of-contents style&lt;/li&gt;
&lt;li&gt;MCP: only what's needed. More connections means more confusion&lt;/li&gt;
&lt;li&gt;Remove stale documents. If AI follows old rules, work goes wrong&lt;/li&gt;
&lt;li&gt;Design human intervention gates explicitly&lt;/li&gt;
&lt;li&gt;Specify verification criteria for each intermediate stage&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  You Don't Have to Be a Developer
&lt;/h2&gt;

&lt;p&gt;Harness engineering is not a concept exclusive to development. The same structure works for general tasks like Excel automation, video production, and data analysis. Even people unfamiliar with vibe-coding can design a harness. You can start by writing a single CLAUDE.md file first.&lt;/p&gt;

&lt;p&gt;The role of developers is changing. From people who write code to people who design environments where agents can work well. The core of AI development has shifted from prompts to harness. If you're just starting out, begin with a single CLAUDE.md.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Q. What is the difference between harness engineering and prompt engineering?
&lt;/h3&gt;

&lt;p&gt;If prompt engineering is the craft of refining what and how to ask AI, harness engineering is the craft of designing the entire environment in which AI works. It includes context files, skills, MCP, hooks, execution loops, verification processes, and human intervention points. A prompt is part of the harness. Harness is a bigger concept than prompts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. How long should CLAUDE.md be?
&lt;/h3&gt;

&lt;p&gt;Under 300 lines is recommended. The longer it is, the more context window it consumes, degrading AI performance. Keeping it short like a table of contents, not a long manual, is key. Writing only "where to find what" is enough. Separate detailed rules into skill files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Is it better to connect more MCPs?
&lt;/h3&gt;

&lt;p&gt;No. Connecting more MCPs leads to token waste and confusion. The principle is to connect only what's needed. The more connections, the higher the cost of AI deciding which tool to use. If it feels like it's being forced, removing it is the right move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Can non-developers apply harness engineering?
&lt;/h3&gt;

&lt;p&gt;Yes. Harness engineering is not a concept exclusive to development. The same structure works for general tasks like Excel automation, video production, and data analysis. Even people unfamiliar with vibe-coding can design a harness. It's simply a matter of documenting what rules AI should follow to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Q. Where should harness design start?
&lt;/h3&gt;

&lt;p&gt;Starting by writing a CLAUDE.md (or Agent.md) file is the fastest approach. Documenting project structure, code rules, and completion criteria in under 300 lines is the first step. MCP or skill files come after. There's no need to have all five elements from the start.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Official Documentation and References&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;Anthropic — Claude Code Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;Anthropic — Hooks Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/skills" rel="noopener noreferrer"&gt;Anthropic — Skills Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.humanlayer.dev/blog/writing-a-good-claude-md" rel="noopener noreferrer"&gt;HumanLayer — Writing a good CLAUDE.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.humanlayer.dev/blog/skill-issue-harness-engineering-for-coding-agents" rel="noopener noreferrer"&gt;HumanLayer — Skill Issue: Harness Engineering&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;The figures in this article (OpenAI Codex 1 million lines, Terminal Bench 2.0, Wharton GAIL CoT study, etc.) are cited from public announcements and papers. Some figures require separate verification from the original papers or official blog posts.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last updated: April 2026. Harness engineering is a rapidly evolving concept. Tools, APIs, and configuration methods can change at any time.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://gocodelab.com/en/blog/en-ai-agent-harness-engineering-guide-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Was Tired of Sorting User Feedback, So I Let AI Classify It</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Sat, 11 Apr 2026 01:00:03 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/i-was-tired-of-sorting-user-feedback-so-i-let-ai-classify-it-5dl5</link>
      <guid>https://forem.com/lazydev_oh/i-was-tired-of-sorting-user-feedback-so-i-let-ai-classify-it-5dl5</guid>
      <description>&lt;p&gt;&lt;em&gt;April 2026 · Lazy Developer EP.05&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In EP.04, I built FeedMission in 7 days. Feedback started coming in. At first, it was great. But once it starts piling up, a different problem emerges. "Please add dark mode." "It hurts my eyes at night." "Add a background color option." Three people said three different things, but the request is the same. Manually grouping these is fine when there are 10. Past 50, just reading them eats up your time.&lt;/p&gt;

&lt;p&gt;I asked Claude: "Can you automatically group similar feedback?" The answer was "embeddings."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick overview&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Convert feedback text into a 1024-number array with Voyage AI (embeddings)&lt;/li&gt;
&lt;li&gt;Simultaneously analyze sentiment scores (-1.0 to 1.0) with Claude Haiku&lt;/li&gt;
&lt;li&gt;Enable the pgvector extension on PostgreSQL for vector storage + similarity search&lt;/li&gt;
&lt;li&gt;Cosine similarity &amp;gt;= 0.85 means same group; below that creates a new group&lt;/li&gt;
&lt;li&gt;When a group grows, Claude automatically re-generates the name and summary&lt;/li&gt;
&lt;li&gt;The entire pipeline runs in the background via the after() pattern&lt;/li&gt;
&lt;li&gt;It all fits in 188 lines of clustering.ts&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  "Group similar feedback" — but how?
&lt;/h2&gt;

&lt;p&gt;"Please add dark mode" and "It hurts my eyes at night" share zero words. But they're the same request. How do you tell a computer that? You convert the sentence into 1024 numbers. Similar meanings produce similar numbers. Think of it like a food delivery app — searching "late night cravings" finds both fried chicken and pork feet at the same time. It searches by meaning, not by matching words.&lt;/p&gt;

&lt;p&gt;"Why not just send two pieces of feedback to Claude and ask if they're similar?" Of course that works. But with 100 feedback items, that's 4,950 comparison pairs. Calling the Claude API for each one is unmanageable in both time and cost. With embeddings, you create them once and the DB handles comparisons with math. Measuring distances between numbers finishes in milliseconds.&lt;/p&gt;

&lt;p&gt;Embeddings aren't a silver bullet, though. They're weak with numbers. "I need 3 buttons" and "I need 30 buttons" have completely different meanings, but their embedding coordinates come out nearly identical. Negation is the same problem. "Dark mode is great" and "Dark mode is terrible" land on similar coordinates. I'm aware of this. But given the nature of feedback, these cases weren't common. Build a structure that covers 80% quickly, and fix the remaining 20% when actual problems arise.&lt;/p&gt;

&lt;h2&gt;
  
  
  My first embedding with Voyage AI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/ai/embeddings.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;https://api.voyageai.com/v1/embeddings&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;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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;voyage-3-lite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input&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="s2"&gt;Please add dark mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// → [0.0234, -0.0891, 0.0412, ...] (1024 numbers)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran sentiment analysis at the same time with Claude Haiku. "Please add dark mode" came back as 0.1 (neutral), "Why isn't this available yet?" came back as -0.4 (negative). Same request, different temperature. Both tasks run simultaneously with &lt;code&gt;Promise.all&lt;/code&gt;. Sequential takes 500ms; parallel takes 300ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  pgvector — storing 1024 numbers in the DB
&lt;/h2&gt;

&lt;p&gt;There are dedicated vector DBs like Pinecone. But I was already using Supabase PostgreSQL. pgvector lets you store and compare vectors in your existing DB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// prisma/schema.prisma
model Feedback {
  embedding  Unsupported("vector(1024)")?  // ← this
  sentiment  Float?
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But Prisma doesn't officially support pgvector. Declaring it as &lt;code&gt;Unsupported&lt;/code&gt; creates the schema, but you can't read or write this column through the normal Prisma API. You have to write raw SQL — a hybrid approach. Not elegant, but it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Find things similar to this" — similarity search
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"Feedback"&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"projectId"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;  &lt;span class="c1"&gt;-- exclude self (important!)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;AND id != $3&lt;/code&gt;. Forgetting this one cost me quite a while. The item matched itself with similarity 1.0. Obviously. It's the most similar to itself. Every new piece of feedback joined an existing cluster, and new clusters never got created. Fixed with one line, but took a while to find.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The first trap in vector similarity search&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you don't exclude the item itself from results, you always get similarity 1.0. Every piece of feedback gets classified as "identical to something that already exists," and new groups never get created. A single line — &lt;code&gt;AND id != $3&lt;/code&gt; — determines the correctness of the entire logic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  0.85 was the answer — how I chose the threshold
&lt;/h2&gt;

&lt;p&gt;I created 20 pieces of test feedback and experimented directly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.70&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Dark mode request" and "UI color change" in the same group — related but different requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.80&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"CSV export" and "data download" in the same group — borderline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.85&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Please add dark mode" and "It hurts my eyes at night" in the same group — correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only nearly identical sentences matched — too strict&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Cluster assignment — join or create
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// clustering.ts:87&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bestMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;similar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;similarity&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clusterId&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;bestMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// join existing group&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ask Claude for a name and create new group&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time a group grows, the name gets regenerated at multiples of 3 (or at 2). Calling Claude every time would inflate API costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Claude names the groups
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Claude response example
{ "title": "Dark Mode / Theme Custom",
  "summary": "Users are requesting dark mode and theme color options" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked well. But occasionally Claude wrapped the JSON in a code block. Running &lt;code&gt;JSON.parse&lt;/code&gt; on that obviously throws an error. I added a defensive regex to strip the code block markers. AI responses are never 100% predictable. You always need a fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Priority — what to look at first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// clustering.ts — priority formula
votes 50% + feedback count 30% + recency 20%

// 50 votes = max score, 10 feedbacks = max score
// recency hits 0 after 50 days since last feedback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This formula produces a score between 0 and 100. 70+ shows in red, 40+ in yellow, below that in green.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzw8o9bl8fcdkrhzjk2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzw8o9bl8fcdkrhzjk2q.png" alt="FeedMission cluster list" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;AI auto-classified cluster list / GoCodeLab&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The after() pattern — users don't wait
&lt;/h2&gt;

&lt;p&gt;This entire pipeline runs inside the feedback submission API. Making the user wait 2 seconds after pressing "Submit Feedback" is bad UX.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/feedback/route.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;feedback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// ↑ immediately return "received"&lt;/span&gt;

&lt;span class="nf"&gt;after&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processFeedbackAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// ↑ embedding + clustering runs in background&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;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzms02m3bm1e4xgiar2ef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzms02m3bm1e4xgiar2ef.png" alt="FeedMission AI pipeline" width="800" height="814"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Feedback → AI clustering pipeline full flow / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Problems I ran into
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-similarity of 1.0&lt;/strong&gt; — Forgot &lt;code&gt;AND id != $3&lt;/code&gt;, so new groups never got created&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NaN mixed into embeddings&lt;/strong&gt; — Without &lt;code&gt;isFinite()&lt;/code&gt; validation, the DB gets corrupted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Priority score of 0 for new groups&lt;/strong&gt; — Only called &lt;code&gt;recalculatePriority()&lt;/code&gt; when joining an existing group&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude wrapping JSON in code blocks&lt;/strong&gt; — Regex stripping + try-catch + fallback are essential&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Automated feedback classification in 188 lines
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clustering.ts&lt;/code&gt; — the entire pipeline fits in 188 lines. 2 external APIs (Voyage + Claude), 5-7 DB queries, 1 branch. All in one file, so the flow is easy to follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is an embedding?&lt;/strong&gt;&lt;br&gt;
It converts a sentence into an array of numbers. Sentences with similar meaning produce similar number patterns, and comparing the numbers lets you calculate "how similar" they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is pgvector needed?&lt;/strong&gt;&lt;br&gt;
It's an extension that adds vector storage + similarity search to existing PostgreSQL. No separate vector DB needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How did you decide on 0.85?&lt;/strong&gt;&lt;br&gt;
Experimented with 20 pieces of feedback. 0.7 was too broad, 0.9 was too narrow. At 0.85, "the same request worded differently" grouped well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you use pgvector with Prisma?&lt;/strong&gt;&lt;br&gt;
No official support yet. Declare the vector column as Unsupported and use raw SQL for reads/writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the after() pattern?&lt;/strong&gt;&lt;br&gt;
A pattern that sends the response first and runs additional work in the background. The user doesn't wait.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-feedmission-ai-feedback-clustering-ep05" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>feedback</category>
      <category>postgres</category>
    </item>
    <item>
      <title>I Built a SaaS in 7 Days</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Sat, 11 Apr 2026 01:00:02 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/i-built-a-saas-in-7-days-25e4</link>
      <guid>https://forem.com/lazydev_oh/i-built-a-saas-in-7-days-25e4</guid>
      <description>&lt;p&gt;&lt;em&gt;April 2026 · Lazy Developer EP.04&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After building Apsity in EP.02, feedback from 12 apps started pouring in. Emails, reviews, DMs. At first I organized them in a spreadsheet. But "please add dark mode" and "my eyes hurt at night" are the same request, just written by different people in different words. Grouping similar requests, assigning priorities, notifying when they're resolved. All manual. The spreadsheet kept growing but never got organized.&lt;/p&gt;

&lt;p&gt;There's a service called Canny. It does exactly this. Feedback collection, voting, roadmap. But the pricing starts at $79/month. Too much for an indie developer. If existing tools are too expensive or don't fit, I build my own.&lt;/p&gt;

&lt;p&gt;I decided to build a SaaS with everything: feedback collection, AI auto-classification, public roadmap, changelog, voting, and email notifications. Named it FeedMission. This post is the record of how it started.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttyx873hto49jd503td9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttyx873hto49jd503td9.png" alt="FeedMission landing page" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The finished FeedMission landing page / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Overview&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Canny at $79/mo → too expensive for indie devs → decided to build it myself&lt;/li&gt;
&lt;li&gt;Designed AI clustering on top of the Next.js + Supabase stack I learned from Apsity&lt;/li&gt;
&lt;li&gt;Handed Claude a structured spec → MVP of 10,742 lines in 52 minutes&lt;/li&gt;
&lt;li&gt;9 DB models, 12 APIs, 8 dashboard pages, widget, AI pipeline — all in one commit&lt;/li&gt;
&lt;li&gt;The real work came after the MVP — structural changes, performance, and security took far more time&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  I defined what I was building
&lt;/h2&gt;

&lt;p&gt;I didn't just tell Claude "make me a feedback tool." I had the Apsity experience. I knew that specific requirements produce specific results. I signed up for Canny, Nolt, and Fider and used them myself. Features they all shared: feedback boards, voting, roadmap, changelog. That's the baseline. But I wanted one more thing — when feedback piles up, automatically group similar items together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// My FeedMission requirements
Core: Feedback collection widget + public board + voting
Management: Roadmap kanban + changelog + email notifications
AI: Auto-classify similar feedback (embeddings + clustering)
AI: Sentiment analysis + auto-generated insights
Revenue: FREE / STARTER $9 / PRO $19 plans
Platforms: Script + React + iOS + Android + iframe + GTM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The MVP came out in 52 minutes
&lt;/h2&gt;

&lt;p&gt;March 26, 9:41 AM. Started the project with &lt;code&gt;create-next-app&lt;/code&gt;. Fed Claude the organized requirements and started building.&lt;/p&gt;

&lt;p&gt;10:33 AM. Pushed the commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;234006b feat: FeedMission full MVP implementation
73 files changed, 10742 insertions(+)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;52 minutes. 73 files. 10,742 lines. Vibe coding is fast, but the reason isn't "Claude wrote the code" — it's "I knew exactly what I was building." When requirements are clear, Claude's output is precise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was inside the MVP
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// 9 DB models (Prisma)
User, Project, Feedback, Cluster, RoadmapItem,
Changelog, Vote, NotificationLog, Subscription

// 12 API routes
/api/feedback — feedback CRUD + widget CORS
/api/clusters — AI cluster view/edit
/api/roadmap — roadmap kanban CRUD
/api/changelog — changelog + auto email on publish
/api/dashboard — stats aggregation (8 queries in parallel)
/api/insights — AI insight card generation

// 8 dashboard pages
Overview, Feedback, Clusters, Roadmap,
Changelog, Notifications, Widget, Settings

// 3 AI pipeline files
clustering.ts — feedback → embedding → cluster assignment
embeddings.ts — Voyage AI vector generation + Claude sentiment analysis
summaries.ts — Claude generates cluster titles/summaries + insights
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Feedback model has an &lt;code&gt;embedding vector(1024)&lt;/code&gt; column. Feedback text gets converted into 1024 numbers via Voyage AI and stored. pgvector handles similarity search on these numbers. "Please add dark mode" and "my eyes hurt at night" end up with similar number patterns and automatically get grouped together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzduauvm3vnkajs0yfxcu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzduauvm3vnkajs0yfxcu.png" alt="FeedMission dashboard" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;FeedMission Dashboard Overview / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between "working code" and "product"
&lt;/h2&gt;

&lt;p&gt;It built successfully. No type errors either. But the moment I actually used it, things to fix started piling up.&lt;/p&gt;

&lt;p&gt;First: the sidebar was eating too much screen space. Switching to a top navigation took 4 minutes. Second: UUIDs were baked into the URLs. I refactored to slug-based routing — 13 files were referencing &lt;code&gt;params.projectId&lt;/code&gt;. Third: after deploying to production, it was slow. The Vercel Function was running in the US, while the Supabase DB was in Seoul. Every query was crossing the Pacific Ocean.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The reality of vibe coding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is why you can't ship AI-generated code as-is. Region settings, middleware optimization, security vulnerabilities, CLS — these only become visible when you actually run and use the code. Claude generates the first draft quickly, and I spend my time asking: "Why is this slow?", "Is this URL structure right?", "Should this data really be exposed?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What happened over the next few days
&lt;/h2&gt;

&lt;p&gt;By midnight on Day 1, I had 5 performance-related commits stacked up. Changed the Vercel region to Seoul (icn1), skipped unnecessary auth calls for public routes in middleware, added Prisma singleton caching, and matched skeleton heights to eliminate layout shift.&lt;/p&gt;

&lt;p&gt;For 5 days I didn't touch the code and just used it myself.&lt;/p&gt;

&lt;p&gt;On Day 6: improved 38 files in one go. 7 security patches, 6 DB indexes, dashboard parallel query optimization. Expanded the widget SDK to 5 types, built iOS SwiftUI and Android Kotlin native widgets. Integrated LemonSqueezy payments and pivoted pricing from KRW to USD. Along the way, I accidentally committed 686K lines of node_modules and pushed a deletion commit 28 seconds later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7awazvzxtrq967z612pd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7awazvzxtrq967z612pd.png" alt="FeedMission 7-day commit timeline" width="800" height="577"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;7-day timeline of 51 commits / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total Commits&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Co-Authored&lt;/td&gt;
&lt;td&gt;37 (72.5%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Active coding days&lt;/td&gt;
&lt;td&gt;3 (out of 7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MVP generation time&lt;/td&gt;
&lt;td&gt;52 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  72.5% was AI, the rest was judgment
&lt;/h2&gt;

&lt;p&gt;37 out of 51 commits have the Claude Co-Authored-By tag. 72.5%. This doesn't mean "Claude built 72.5% of it." I organize the requirements, Claude generates code, I review, modify, and commit.&lt;/p&gt;

&lt;p&gt;This is why vibe coding isn't "letting AI do everything." Build fast, use it fast, decide fast. What speeds up isn't code generation — it's the entire feedback loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is a 52-minute MVP actually usable?&lt;/strong&gt;&lt;br&gt;
"Working code" came out in 52 minutes. But bringing it to product quality took the remaining 6 days. The MVP is a starting point, not the finish line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is building your own better than using Canny?&lt;/strong&gt;&lt;br&gt;
Depends on team size and budget. If $79/month is a stretch and you need custom features like AI auto-classification, building your own might be the way to go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does AI clustering work?&lt;/strong&gt;&lt;br&gt;
Feedback text gets converted into 1024 numbers (embeddings). Sentences with similar meanings produce similar number patterns. Comparing them and grouping items with similarity above 0.85 into the same cluster. Covered in detail in EP.05.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is code quality from vibe coding acceptable?&lt;/strong&gt;&lt;br&gt;
It works at the MVP stage, but you can't ship it as-is. I separately fixed 7 security vulnerabilities and 4 performance issues. AI generates the first draft, but human review is always required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-feedmission-saas-7days-mvp-ep04" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>startup</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Dashboard Was There But I Didn't Know What to Do, So I Let AI Handle It</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Fri, 10 Apr 2026 01:57:29 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/the-dashboard-was-there-but-i-didnt-know-what-to-do-so-i-let-ai-handle-it-39f</link>
      <guid>https://forem.com/lazydev_oh/the-dashboard-was-there-but-i-didnt-know-what-to-do-so-i-let-ai-handle-it-39f</guid>
      <description>&lt;p&gt;&lt;em&gt;March 2026 · Lazy Developer EP.03&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I had a dashboard. Built it in EP.02. Every day at 3 AM, a Cron job runs. By morning, all of yesterday's data for 12 apps is there. Downloads total, revenue, keyword rankings. Everything on one screen. It took days to build, and it saved me 15 minutes every morning.&lt;/p&gt;

&lt;p&gt;But about three days in, a different kind of question lingered. "The finance app downloads dropped 22% today." The number was right there on screen. So what? I didn't know why it dropped. I didn't know what to do about it. The dashboard showed me what happened. The judgment was still on me. Having a dashboard actually made things more tiring in a way — the decisions I needed to make became painfully clear.&lt;/p&gt;

&lt;p&gt;So I decided to automate the judgment part too. I built an AI growth agent inside Apsity. That's what this post is about.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Overview&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard shows "what happened," but "why" and "what to do" were still on me&lt;/li&gt;
&lt;li&gt;Designed 5 analysis patterns: rank drop diagnosis, hidden markets, keyword optimization, review analysis, revenue breakdown&lt;/li&gt;
&lt;li&gt;Confidence badge on every insight: Fact / Correlation / Suggestion&lt;/li&gt;
&lt;li&gt;Indie app filter excludes enterprise apps (1,000+ ratings), analyzes only comparable apps&lt;/li&gt;
&lt;li&gt;Second Claude API integration — auto-generates 100-character keyword sets, suggests app names, extracts insights&lt;/li&gt;
&lt;li&gt;Auto growth stage detection: SEED -&amp;gt; GROWING -&amp;gt; STABLE&lt;/li&gt;
&lt;li&gt;Weekly email report added: Monday 8 AM, auto-sent via Resend + React Email&lt;/li&gt;
&lt;li&gt;First run: 12 apps, 48 insights generated automatically&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What existing tools don't tell you — and the price tag
&lt;/h2&gt;

&lt;p&gt;There are plenty of App Store analytics tools out there. AppFollow, Sensor Tower, MobileAction, plus App Store Connect itself. They all do the same thing. "3,240 downloads this week." "Keyword ranking change: -5." Numbers showing what happened.&lt;/p&gt;

&lt;p&gt;The pricing tells the story. Sensor Tower's enterprise plan starts at $30,000/year. AppFollow has a $39/month basic plan, but it caps at 5 apps. Managing 12 means upgrading, and the cost jumps. So most indie developers end up using App Store Connect's built-in analytics.&lt;/p&gt;

&lt;p&gt;No matter which tool you use, the "so what?" question remains. Why did it drop? Did a competitor change something? Is my keyword the problem? Is there a signal in the reviews? To figure that out, you have to dig through the data yourself. The tools don't dig for you.&lt;/p&gt;

&lt;p&gt;What I wanted was different. Give it data, and it tells me the cause. If there's a cause, it tells me what to do. If it knows what to do, it gives me something I can use right now. Not "you might want to change your keywords" but "copy this 100-character set and paste it into your App Store keyword field."&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Growth Agent — a system that makes judgments for you
&lt;/h2&gt;

&lt;p&gt;I wrote up my requirements and handed them to Claude. "Not just showing what happened — diagnose the cause, provide verifiable evidence, and deliver ready-to-use outputs." It was a one-line description. Claude broke it into 5 analysis patterns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// 5 Analysis Patterns
1. Rank Drop Diagnosis — Why it dropped, including competitor changes
2. Hidden Market Discovery — Keywords where my app isn't showing but opportunities exist
3. Keyword Optimization — Current keyword analysis + auto-generated 100-char optimal set
4. Review Keyword Analysis — Recurring patterns extracted from user reviews
5. Revenue Breakdown — Subscription vs IAP anomaly detection + cause hypotheses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Patterns alone are meaningless. What matters is how trustworthy each result is. Not everything AI says is fact. Something read directly from data, something inferred from patterns, and something AI suggests — these are fundamentally different. Without distinguishing them, you'd treat inferences as facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I added confidence badges
&lt;/h2&gt;

&lt;p&gt;I attached a confidence badge to every insight card. There are three types.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Badge&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fact&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Directly confirmed from real data. Like "downloads dropped 22% yesterday" — a measured figure.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Correlation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inferred from patterns between data points. Like "competitor updated their description right before your ranking dropped" — related but not causally confirmed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Suggestion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI reasoning based on analysis. Like "adding this keyword could increase impressions" — data-informed but not certain.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each card also has a [View Evidence] toggle. Click it, and you see the raw data: "34% drop from 7-day download average, competitor A changed 3 metadata fields in the same period." You can check what data the AI used to produce the insight. So you can judge for yourself whether to trust it.&lt;/p&gt;

&lt;p&gt;This is a design decision, but it's also a philosophy. You shouldn't just follow what AI says. You should be able to see why it said it. That way, you'll know when it's wrong, too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indie app filter — wrong comparisons make analysis useless
&lt;/h2&gt;

&lt;p&gt;While designing the competitive analysis, I hit a problem. Even within the same category, some apps shouldn't be compared. Official apps from major banks, apps from companies like Naver or Kakao. They have different marketing budgets, different ASO strategies, and hundreds of thousands of ratings. If an indie developer gets compared against them by the same standards, no meaningful insight comes out.&lt;/p&gt;

&lt;p&gt;I asked Claude, and it suggested a rating-count filter. Apps with 1,000+ ratings get classified as enterprise and excluded from comparisons. Apps with 50-1,000 ratings get classified as indie successes and used as the comparison baseline.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The indie app filter logic is simple. On the App Store, an app's rating count correlates with downloads. Over 1,000 ratings means significant marketing investment — that's not indie. Meanwhile, 50-1,000 ratings means somewhat validated but still indie-scale. That's the range you actually want to compare against.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Competitors menu — checking every day if someone changed something yesterday
&lt;/h2&gt;

&lt;p&gt;Once you register a competitor, a Cron job calls the iTunes Lookup API every day at 4 AM KST to fetch that app's latest metadata. App name, subtitle, description, icon, version. These five fields get saved daily, compared against the previous day, and any changes get logged.&lt;/p&gt;

&lt;p&gt;Open the menu and you see the list of registered competitors. Apps with recent metadata changes rise to the top, showing which fields changed. Click on a changed field to see the previous and current versions side by side.&lt;/p&gt;

&lt;p&gt;At first I thought, "Does this even matter?" So a competitor changed their description — what can I do about it? Using it changed my mind. Three competitors of my finance app updated their descriptions and keywords on the same day, and my ranking dropped right after. The MetaChange log had the dates and exact changes. It's correlation, not causation, but without this data, tracking down the cause would have taken much longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugging Claude API into Apsity — in the Keywords menu
&lt;/h2&gt;

&lt;p&gt;I added two more specific features. Auto-generating an optimal 100-character keyword set, and suggesting app names and subtitles based on indie success patterns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST /api/growth/keywords-generate&lt;/span&gt;
&lt;span class="c1"&gt;// App name, category, current keywords -&amp;gt; Claude -&amp;gt; 100-char optimal keyword set&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
App: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;appName&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;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)
Current keywords: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;currentKeywords&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Top indie app keyword patterns: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;indiePatterns&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

Generate an optimized keyword set within 100 characters for the App Store keyword field.
Remove duplicate words, separate with commas, no spaces after commas.`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are rules for the keyword field:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No space after commas (even one space counts toward the character limit)&lt;/li&gt;
&lt;li&gt;No plurals (App Store auto-matches from singular)&lt;/li&gt;
&lt;li&gt;Don't repeat app name or category name (they're already indexed)&lt;/li&gt;
&lt;li&gt;Fill all 100 characters (empty space = wasted exposure)&lt;/li&gt;
&lt;li&gt;Include review keywords (frequent words from review text act as search signals)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frm6np2heo38m6wcz6c8d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frm6np2heo38m6wcz6c8d.png" alt="Apsity keyword optimization page" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Keyword Optimization — Copy the AI-generated 100-character optimal set with one click / GoCodeLab&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Adaptive growth stage mode — from SEED to STABLE
&lt;/h2&gt;

&lt;p&gt;After building all the analysis features, one problem became obvious. Running "revenue anomaly detection" on a freshly launched app is pointless. There's no data. On the flip side, only running basic keyword generation for a well-established app is a waste.&lt;/p&gt;

&lt;p&gt;Claude proposed auto-detecting growth stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌱 &lt;strong&gt;SEED&lt;/strong&gt; — Less than 30 days of downloads or under 500 cumulative. Focuses on initial setup: keyword auto-generation, app name suggestions.&lt;/li&gt;
&lt;li&gt;🌿 &lt;strong&gt;GROWING&lt;/strong&gt; — Download trend is rising or stable. Rank drop diagnosis, hidden market discovery, and competitor change detection all activate.&lt;/li&gt;
&lt;li&gt;🌳 &lt;strong&gt;STABLE&lt;/strong&gt; — Over 3 months of accumulated data. Revenue anomaly detection, review keyword analysis, and long-term trend pattern analysis activate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnuwt3ihcsrjg2ws3mygo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnuwt3ihcsrjg2ws3mygo.png" alt="Apsity growth stage overview" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Auto growth stage detection — each stage activates different analyses / GoCodeLab&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Claude reviewed my code
&lt;/h2&gt;

&lt;p&gt;I tried something new this time. I had Claude review the code it wrote. After everything was built, I said: "Review this entire codebase. Focus on things that could break in production."&lt;/p&gt;

&lt;p&gt;The results were more specific than I expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// [Critical] 1st Review — Key Issues
1. MetaChange relation missing — DB save without linking relation table
2. JSON.parse unprotected — No try-catch on external API response parsing
3. Cron timeout — Timeout risk when processing 12 apps sequentially

// [Critical] 2nd Review — Key Issues
4. iTunes API rate limit — 429 risk from calling in a loop with no delay
5. Review country hardcoded — Only collecting KR, missing other countries
6. ASC data delay — Yesterday's data may not be available at early morning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There was a strange feeling. Code written by Claude, reviewed by Claude, bugs found by Claude, fixed by Claude. The line between what I built and what it built got even blurrier. But I'll take that feeling over things breaking in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Weekly email report — a summary arriving Monday at 8 AM
&lt;/h2&gt;

&lt;p&gt;Insights were being generated, but you could only see them by opening the dashboard. I set up weekly reports to be emailed automatically using Resend + React Email + Vercel Cron.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vercel.json — Cron schedule&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crons&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/cron/collect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;schedule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 18 * * *&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;      &lt;span class="c1"&gt;// 3 AM KST daily&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/cron/analyze&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;schedule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;30 10 * * *&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;     &lt;span class="c1"&gt;// 7:30 PM KST daily&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/cron/weekly-report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;schedule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 23 * * 0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// Monday 8 AM KST&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 email contains last week's per-app download and revenue summary, the top 3 insights (with confidence badges), and one immediately actionable item. Long emails don't get read, so the goal was to fit everything on one screen without scrolling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrwzhqnc3mikjcxetkuv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkrwzhqnc3mikjcxetkuv.png" alt="Apsity weekly email report" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Weekly email report — auto-sent Monday 8 AM, everything fits on one screen / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First run — 48 insights
&lt;/h2&gt;

&lt;p&gt;I deployed the Cron and triggered it manually. 12 apps processed, total execution time 38 seconds. 48 insights generated.&lt;/p&gt;

&lt;p&gt;Different kinds of insights came in for each app. The finance app was STABLE, so revenue anomaly detection ran. The habit tracker was GROWING, so competitor change detection ran alongside it. A recently launched app was SEED, so only keyword auto-generation insights came through.&lt;/p&gt;

&lt;p&gt;One insight caught my eye. "Over the past 14 days, 3 competitors simultaneously updated their metadata for the 'budget' keyword cluster, and your app's ranking for those keywords dropped an average of 8 positions since." Confidence badge: Correlation. I clicked [View Evidence] and verified the data manually. It checked out.&lt;/p&gt;

&lt;p&gt;Below that insight card was a keyword set with a copy button. Claude had generated it incorporating the competitive keyword changes. I copied it and pasted it into App Store Connect. The flow itself — change detection, cause hypothesis, response keyword generation, copy — all happened automatically. That's the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q. What exactly is an AI growth agent?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Existing tools show you numbers. "Downloads down 22%." The AI growth agent goes one step further. It proposes a hypothesis for why it dropped, shows the supporting data, and produces a ready-to-use deliverable for your response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. How do the confidence badges work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fact means read directly from data. Correlation means inferred from patterns between two data points. Suggestion means AI reasoning. Check the badge and decide how much to trust it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Why is the indie app filter based on rating count?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rating count correlates with downloads. Over 1,000 means significant marketing has already gone in, and indie developers shouldn't benchmark against that. The 50-1,000 rating range represents apps that succeeded under similar conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. How are the growth stages determined?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They're auto-detected every time the daily Cron runs. It evaluates data collection duration, cumulative downloads, and recent trend direction. When the stage changes, the types of analysis change too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Can vibe coding really produce features like this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built it, so yes. The key is being clear about what you want. Claude structured the code and wrote most of it, but the decisions about which features were needed, ideas like confidence badges — those were mine. It's become more about judgment than coding skill.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-apsity-ai-growth-agent-insights-ep03" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>ai</category>
      <category>appstore</category>
      <category>analytics</category>
    </item>
    <item>
      <title>I Got Tired of Checking Revenue for 12 Apps, So I Built My Own Dashboard</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Fri, 10 Apr 2026 01:57:26 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/i-got-tired-of-checking-revenue-for-12-apps-so-i-built-my-own-dashboard-86e</link>
      <guid>https://forem.com/lazydev_oh/i-got-tired-of-checking-revenue-for-12-apps-so-i-built-my-own-dashboard-86e</guid>
      <description>&lt;p&gt;&lt;em&gt;March 2026 · Lazy Developer EP.02&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have 12 apps on the App Store. It started with one. One becoming two was natural. Two becoming five was ambition. Five becoming twelve was the result of not stopping. Having more apps isn't bad — the problem is that the number of things to check grows at the same rate.&lt;/p&gt;

&lt;p&gt;I built a dashboard that shows all 12 apps' revenue, downloads, and keyword rankings on a single screen. I named it Apsity. I connected the App Store Connect API and set it up to pull data automatically every night. This post is the dev journal of that process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa57c6c6bk5z80hembl9i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa57c6c6bk5z80hembl9i.png" alt="Apsity — a custom App Store Connect dashboard showing revenue and downloads for 12 apps on a single screen" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The dashboard I built — data from all 12 apps on one screen / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Overview&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App Store Connect's Sales and Trends aggregation is being deprecated between 2026–2027&lt;/li&gt;
&lt;li&gt;Decided to build my own data pipeline before it disappears&lt;/li&gt;
&lt;li&gt;Threw two lines of requirements at Claude and locked in the Next.js + Supabase + Vercel architecture&lt;/li&gt;
&lt;li&gt;Connected the App Store Connect API with JWT authentication, parsed TSV data&lt;/li&gt;
&lt;li&gt;Vercel Cron auto-syncs at 3 AM daily, after() pattern handles 12 apps in parallel&lt;/li&gt;
&lt;li&gt;Now when I wake up, yesterday's data is already there&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  What's the Problem with App Store Connect?
&lt;/h2&gt;

&lt;p&gt;Let me clear something up first. App Store Connect's Sales and Trends does let you view aggregated data across all your apps. As of now, you can see Units, Proceeds, and Sales bundled for all apps. No need to click into each one — the totals are right there. So saying "ASC can't aggregate" would be wrong today.&lt;/p&gt;

&lt;p&gt;The problem is that this is about to go away. In March 2026, Apple announced a major Analytics overhaul and said they're phasing out the Sales and Trends dashboard. Subscription dashboards start disappearing mid-2026, and the rest follows through 2027. The new Analytics comes with over 100 per-app metrics — cohort analysis, peer benchmarks, subscription data. But the ability to view multiple apps aggregated on a single screen? Gone.&lt;/p&gt;

&lt;p&gt;John Voorhees at MacStories nailed it: "There will no longer be a single place to see aggregated performance across multiple apps." Steve Troughton-Smith put it as "App Store Connect had a big scary overhaul and now everything is in the wrong place." Apple acknowledged the feedback about cross-app reporting, but acknowledging and fixing are two different things.&lt;/p&gt;

&lt;p&gt;I decided to build my own before it all disappears. What I wanted was more specific than what ASC offered anyway. Not just aggregated numbers — I wanted keyword ranking, competitor tracking, and AI analysis, all in one place. The aggregated dashboard was the most urgent piece, so I started there.&lt;/p&gt;
&lt;h2&gt;
  
  
  I Threw the Requirements at Claude
&lt;/h2&gt;

&lt;p&gt;I didn't architect this alone. I opened a fresh Claude session and just wrote what I needed. "I'm a developer running multiple iOS apps and I want to see revenue on a single screen. App Store Connect API integration. Keyword ranking." Two lines.&lt;/p&gt;

&lt;p&gt;Claude came back with a stack proposal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Initial architecture proposed by Claude
Framework: Next.js — builds the frontend and backend in one package
DB: Supabase — cloud database with real-time sync, like a spreadsheet but smarter
Auth: Supabase Auth — handles login/session management automatically
Deploy: Vercel — push code and it's live on the internet, with built-in timers
Charts: Recharts — a library for rendering graphs and charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the first evening, the basic structure was in place. Things moved so fast that I actually had to make more decisions, not fewer. Claude recorded those decisions in CLAUDE.md. Even when sessions were interrupted, context like "why we recreated the Supabase project in US East instead of the India region" was preserved. The automation I built in EP.01 paid off right here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting the App Store Connect API — JWT Authentication Was the First Wall
&lt;/h2&gt;

&lt;p&gt;You can't just access the App Store Connect API. You have to generate a JWT token yourself. JWT is like a digital ID card that proves "I'm really the developer." You sign it with a private key issued by Apple, include an expiration time, and match Apple's exact encryption method (ES256). Reading about it makes sense, but when you actually build it, something always goes wrong.&lt;/p&gt;

&lt;p&gt;I handed Claude the Apple documentation link and said, "Generate this JWT in TypeScript." I pasted the code it returned, ran it, and the first API call went through. A response came back. In TSV format. TSV is a text file with tab-separated columns — like a spreadsheet but in plain text. When that wall of text hit my terminal, I knew it was just data, but it still felt good. Something was connected.&lt;/p&gt;

&lt;p&gt;The next problem was parsing that data. The sales report has dozens of columns. The key one is a field called productType — a single number. 1 means download, 7 means in-app purchase, 8 means subscription. Apple uses this number to classify transaction types. If you scatter this logic throughout your code, you'll have to hunt down every instance when Apple changes the spec. Claude suggested creating a single classification function called &lt;code&gt;categorize()&lt;/code&gt;. All type detection goes through this one function, and the rest of the code just reads the result. It still runs that way today.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Currency Conversion Strategy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Revenue aggregation across countries is trickier than it sounds. Sales in Korea come in KRW, sales in Japan come in JPY. If you convert manually using exchange rates, yesterday's revenue might look lower than today's just because of currency fluctuation. Apple already provides a USD-converted value in the proceeds field. Claude suggested aggregating only that value. Consistent dollar-based trends with no exchange rate distortion. Made sense, so I used it as-is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Vercel Cron Automation — Data Piles Up While I Sleep
&lt;/h2&gt;

&lt;p&gt;The API was connected. I could read the data. Now I needed this to run automatically every day. Vercel has a feature called Cron. Like a smartphone alarm — you set "run this at this time every day" and it handles the rest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vercel.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cron&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;schedule&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"/api/cron/daily-sync"&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;day&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;AM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;KST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(UTC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"/api/cron/daily-rank"&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Every&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;day&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;AM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;keyword&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ranking&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;collection&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"/api/cron/cleanup"&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Weekly&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;auto-purge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;old&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3 AM in Korea is 18:00 UTC. I timed it for when the previous day's data is finalized. The first night after deploying, I checked the Vercel logs from bed. The Cron had run, the API had been called, and data was sitting in Supabase. When I opened the dashboard that morning, yesterday's download numbers were already there. I hadn't done anything, but the data was there. That feeling was stranger than I expected. In a good way.&lt;/p&gt;

&lt;p&gt;Then a problem appeared. With 12 apps, the sync job needs to run 12 times. Vercel Cron kills any job that doesn't finish within 10 seconds. Processing them one by one takes over 20 seconds. If it gets cut off at 10 seconds, the remaining apps don't get synced.&lt;/p&gt;

&lt;p&gt;I explained the situation to Claude, and it suggested the &lt;code&gt;next/server after()&lt;/code&gt; pattern. Like a cashier at a convenience store saying "thank you" the moment payment is done, then printing the receipt afterward — respond first, then process in the background.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Respond immediately → process 12 apps in parallel in the background&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;after&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="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apps&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;app&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;syncApp&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="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// Respond immediately before timeout&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns "job complete" instantly. From Vercel's perspective, a response came back, so no timeout. The actual sync runs in the background inside &lt;code&gt;after()&lt;/code&gt;. All 12 apps start at the same time. Like opening 12 bank teller windows at once instead of making 12 people wait in a single line. Applied it, and the timeout was gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Inside the Dashboard
&lt;/h2&gt;

&lt;p&gt;Here's what the finished product looks like. When you open the dashboard, the first thing you see is four metric cards: total downloads today, total revenue (USD), active subscribers, and best keyword ranking. Each card has a small 7-day trend graph inside it.&lt;/p&gt;

&lt;p&gt;Scroll down and there's a per-app performance table. App name, yesterday's downloads, yesterday's revenue — all in one row, sorted by revenue descending. The Revenue tab shows a stacked chart over 30 days with app sales, in-app purchases, and subscriptions color-coded. The Keywords tab shows a scatter plot of keyword rank vs. difficulty. X-axis is difficulty, Y-axis is rank. Upper-left quadrant is the sweet spot — low difficulty, high rank.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Keyword opportunity score calculation&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;opportunity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;difficulty&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="c1"&gt;// difficulty = based on rating counts of the top 10 apps for that keyword&lt;/span&gt;
&lt;span class="c1"&gt;// fewer ratings means indie apps can break through&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;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hard&lt;/span&gt;&lt;span class="dl"&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;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;             &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Easy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also built a competitor tracking tab. Register up to 5 apps in the same category, and it compares their metadata changes (name, subtitle, description) daily. The Countries tab shows which countries are driving download growth. The Subscriptions tab shows active subscriber counts and renewal rates. Some of this overlaps with what ASC's new Analytics provides per-app. The difference is seeing all 12 of my apps in one place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa864a407ik1l92q4nb0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa864a407ik1l92q4nb0.png" alt="Apsity Revenue tab — App Store Connect subscription and IAP revenue 30-day trend chart" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Revenue tab — 30 days of revenue auto-aggregated by type / GoCodeLab&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Data Gets Stored
&lt;/h2&gt;

&lt;p&gt;Data started flowing in daily as the Cron ran. But accumulating data creates new problems. DB costs go up, old data rarely gets looked at, and cleaning it up manually is yet another chore I don't want. That's where the cleanup Cron came from. Every Sunday at dawn, it automatically deletes data beyond a certain age. Claude suggested this one first. On my own, I probably wouldn't have thought about it until the data had piled up for months.&lt;/p&gt;

&lt;p&gt;Keyword ranking collection had its own quirks. If you call the iTunes Search API too fast inside a loop, you get hit with 429 (rate limit). I fixed it by adding a 300ms delay between requests. ASC data also has occasional delays — yesterday's data might not be available at 3 AM. I added retry logic so it tries again 6 hours later if the data isn't ready.&lt;/p&gt;

&lt;p&gt;This is the part where vibe coding isn't just "Claude writes code so it's fast." It catches future pain points early. Cleanup Cron, rate limit protection, retry logic — I would have only built these after something broke if I'd been working alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The App Works for Me Now
&lt;/h2&gt;

&lt;p&gt;Here's how Apsity runs today. Every day at 3 AM KST, the Cron calls the App Store Connect API. Downloads, revenue, and country-level data from the previous day land in Supabase. 30 minutes later, keyword rankings are collected. When I open the dashboard in the morning, all 12 apps' data from the day before is already there. Aggregation is automatic. Ranking changes are visible immediately.&lt;/p&gt;

&lt;p&gt;It took a few days to build. After that, I got back 15 minutes every morning. 15 minutes times 365 days is 91 hours. 91 hours is enough to build the next app. And when Apple finally sunsets Sales and Trends, there will be no way to see cross-app aggregation in ASC. Having my own pipeline before that happens is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  If It's Tedious, Just Build It
&lt;/h2&gt;

&lt;p&gt;There's a pattern that keeps repeating throughout this series. If something is tedious, build a solution. Right now, building things is a lot less tedious than it used to be. Even if terms like JWT, Cron, and after() feel unfamiliar, it doesn't matter. Ask Claude "what is this and how do I use it?" and it explains and writes the code. Hand it a link to official docs and it reads them and implements. When the timeout issue came up, I just described the situation and the fix appeared. My job is judgment — is this the right direction? Does this approach fit our situation?&lt;/p&gt;

&lt;p&gt;After building Apsity, the next tedious thing appeared. The dashboard showed data, but the question "so what should I do?" remained. The numbers were going down, sure, but figuring out why was still on me. In EP.03, I talk about the AI growth agent that automates even that judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q. Can't you already see aggregated revenue across all apps in App Store Connect?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Currently, Sales and Trends does support cross-app aggregation. But Apple announced they're phasing out Sales and Trends starting mid-2026. The new Analytics only supports per-app analysis — cross-app aggregation is missing. It works now, but it won't for long. I built my own pipeline before it disappears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Isn't connecting the App Store Connect API difficult?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's unfamiliar at first. You need JWT token generation, ES256 signing, and TSV parsing. But give Claude the Apple documentation link and it writes the code right away. Half a day and the API connection was working. Much faster than digging through the docs yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Do you need coding skills to build an app with vibe coding?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Having a clear idea of what you want matters more than coding skills. If you can describe what you want to build in plain language, Claude handles a significant portion. Making judgment calls and setting the direction is still on you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Can you automate Cron jobs on Vercel's free plan?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The free plan has a short timeout, but if you use the after() pattern — respond immediately and process in the background — you can sync all 12 apps without hitting the timeout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. Wouldn't it be better to just use an existing ASO tool instead of building your own?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depends on what you need. Tools like Sensor Tower or AppFollow are feature-rich but can be expensive for indie developers. If you want exactly the features you need and nothing more, building your own can make sense. And the process of building it is a learning experience in itself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-apsity-app-store-analytics-dashboard-ep02" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>ai</category>
      <category>appstore</category>
      <category>analytics</category>
    </item>
    <item>
      <title>AI Writing Tools Compared: ChatGPT vs Claude vs Gemini vs Notion AI (2026)</title>
      <dc:creator>LazyDev_OH</dc:creator>
      <pubDate>Fri, 10 Apr 2026 01:40:29 +0000</pubDate>
      <link>https://forem.com/lazydev_oh/ai-writing-tools-compared-chatgpt-vs-claude-vs-gemini-vs-notion-ai-2026-3cje</link>
      <guid>https://forem.com/lazydev_oh/ai-writing-tools-compared-chatgpt-vs-claude-vs-gemini-vs-notion-ai-2026-3cje</guid>
      <description>&lt;p&gt;In an 8-round blind test that started with 134 participants (dropping to 111 by round 8), Claude won 4 rounds, Gemini 3, and ChatGPT 1. For pure writing quality, Claude is the current leader.&lt;/p&gt;

&lt;p&gt;But writing quality isn't everything. Here's how all four compare.&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;&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;ChatGPT&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;th&gt;Notion AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Writing Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;#1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;#2&lt;/td&gt;
&lt;td&gt;#3&lt;/td&gt;
&lt;td&gt;#4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Versatility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Best&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Research&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Web search&lt;/td&gt;
&lt;td&gt;Web + images&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Google integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;200K (Pro) / 1M (Max+)&lt;/td&gt;
&lt;td&gt;128K&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2M tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Image Gen&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GPT Image built-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$20/mo&lt;/td&gt;
&lt;td&gt;$20/mo&lt;/td&gt;
&lt;td&gt;$19.99/mo (AI Pro)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$10 add-on (legacy)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Claude — Writing Quality Champion
&lt;/h2&gt;

&lt;p&gt;In a blind test (source: aiblewmymind, 134 participants in round 1, 111 by round 8), Claude won 4 of 8 rounds. The key strength is &lt;strong&gt;tone consistency&lt;/strong&gt; — even in long pieces, the writing doesn't drift. "Least AI-sounding" was the most common feedback.&lt;/p&gt;

&lt;p&gt;Pro defaults to a 200K token context, while Max, Team, and Enterprise unlock 1M. That's enough reference material for style-matched output. The downside: no image generation. You'll need ChatGPT or Gemini for visuals.&lt;/p&gt;

&lt;h2&gt;
  
  
  ChatGPT — The Swiss Army Knife
&lt;/h2&gt;

&lt;p&gt;Writing, image generation (GPT Image), web search, code execution, data analysis — all in one place. Need a chart while drafting? Just ask. Deep Research mode can search the web, compile sources, and write a draft in one go.&lt;/p&gt;

&lt;p&gt;Writing quality is a step below Claude, but it's more than good enough for short-form: social media copy, emails, ad text. Hundreds of millions of weekly users means the largest ecosystem of plugins and references.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemini — Research-Powered Writing
&lt;/h2&gt;

&lt;p&gt;The strength is Google integration. Gmail, Google Docs, Google Search — all connected. "Summarize project-related emails from the last 3 months into a report" actually works. Note: Google rebranded Gemini Advanced to &lt;strong&gt;Google AI Pro&lt;/strong&gt; at I/O 2025, priced at $19.99/month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2 million token context&lt;/strong&gt; (on Gemini 1.5 Pro) is the real weapon for writing. Feed in 10 papers at once and get a synthesis. For academic writing or research-heavy reports, nothing else comes close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notion AI — Your Workspace Knows Context
&lt;/h2&gt;

&lt;p&gt;Different from the other three. Not a standalone AI — it's an assistant attached to your Notion workspace. It reads your project databases, meeting notes, and docs.&lt;/p&gt;

&lt;p&gt;"Summarize the status of 50 tasks and identify blockers" — Notion AI can do this because it already has the context. Standalone writing quality falls behind, but &lt;strong&gt;in-context writing&lt;/strong&gt; is where it shines. Legacy subscribers can still attach AI for $10/month, but starting in 2026 new Free/Plus users can't buy the add-on — AI is bundled only into the Business plan ($20/user/month).&lt;/p&gt;

&lt;h2&gt;
  
  
  Blind Test Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Wins (out of 8)&lt;/th&gt;
&lt;th&gt;Key Feedback&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Most natural, minimal editing needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Well-structured, consistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Fast and versatile&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which One to Pick
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blog posts, essays, long-form&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Natural tone, consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social copy, emails, ads&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast, varied outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Research papers, reports&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Gemini&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2M context, Google integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team docs, meeting notes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Notion AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Workspace context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writing + images together&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GPT Image built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You don't have to pick just one. Write the draft in Claude, generate images in ChatGPT, fact-check with Gemini. $40–60/month gets you the full AI writing stack.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://gocodelab.com/en/blog/en-ai-writing-tools-chatgpt-claude-gemini-notion-comparison-2026" rel="noopener noreferrer"&gt;GoCodeLab&lt;/a&gt;. Blind test source: aiblewmymind (134 participants in round 1 → 111 by round 8, Feb 2026, 8 rounds).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
