<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Somnath Khadanga</title>
    <description>The latest articles on DEV Community by Somnath Khadanga (@somnath_khadanga_2e5c2364).</description>
    <link>https://dev.to/somnath_khadanga_2e5c2364</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%2F2272171%2F90243b25-39b8-4fed-90a7-3aa283977054.jpg</url>
      <title>DEV Community: Somnath Khadanga</title>
      <link>https://dev.to/somnath_khadanga_2e5c2364</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/somnath_khadanga_2e5c2364"/>
    <language>en</language>
    <item>
      <title>What to Audit in a Vibe-Coded MVP Before Real Users See It</title>
      <dc:creator>Somnath Khadanga</dc:creator>
      <pubDate>Sat, 09 May 2026 17:14:14 +0000</pubDate>
      <link>https://dev.to/somnath_khadanga_2e5c2364/what-to-audit-in-a-vibe-coded-mvp-before-real-users-see-it-dpi</link>
      <guid>https://dev.to/somnath_khadanga_2e5c2364/what-to-audit-in-a-vibe-coded-mvp-before-real-users-see-it-dpi</guid>
      <description>&lt;p&gt;You built it with Cursor. Or Claude Code. Maybe both. The product works — users can sign up, the core workflow runs, and you've shown it to a dozen people without anything catching fire.&lt;/p&gt;

&lt;p&gt;Now you want to share it more widely.&lt;/p&gt;

&lt;p&gt;Before you do, run this audit.&lt;/p&gt;

&lt;p&gt;This is not a "rewrite everything" post. Most AI-generated code is structurally fine. The problem is almost never the code itself — it's the things the AI didn't know to think about: your deployment environment, your specific threat model, what happens when a real user does something unexpected at 2am.&lt;/p&gt;

&lt;p&gt;I've reviewed several AI-assisted codebases in the last few months. The same problems show up in almost all of them. They're not hard to fix. They're just not obvious when you're moving fast.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p&amp;gt;AI coding tools are genuinely fast. They're not good at knowing what they don't know about your specific production context.&amp;lt;/p&amp;gt;



&amp;lt;p&amp;gt;The things that break in production are almost never the core feature. They're the edges around it that nobody vibe-coded.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Auth: The Most Common Breaking Point
&lt;/h2&gt;

&lt;p&gt;AI tools write auth that works for the happy path. It's the unhappy paths that cause incidents.&lt;/p&gt;

&lt;p&gt;Here's what to check:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Middleware protection is consistent, not selective.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Next.js, a common pattern is protecting routes via &lt;code&gt;middleware.ts&lt;/code&gt; — but then having API routes that don't re-verify the session. If someone bypasses your frontend and hits &lt;code&gt;/api/admin/users&lt;/code&gt; directly, does the route independently check auth?&lt;/p&gt;

&lt;p&gt;Every API route that touches user data should verify the session or token independently of whatever the middleware does. Middleware is a convenience layer, not a security boundary.&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;// This pattern from AI tools is not enough&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;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// If middleware already checked, is this safe?&lt;/span&gt;
  &lt;span class="c1"&gt;// No — middleware can be bypassed, skipped, or misconfigured&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// This is what you actually want&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;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getServerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&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;users&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Role checks happen on the server, not just the client.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your app shows an admin dashboard only when &lt;code&gt;user.role === 'admin'&lt;/code&gt; in a React component, that's UI gating — not access control. The underlying API calls that populate the admin dashboard still need server-side role verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sessions expire and get revoked correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask this: if you manually delete a user's session from the database, will they be logged out on their next request? Or will their existing cookie still work? AI-generated session handling often doesn't account for forced logout, user banning, or credential rotation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Password reset tokens are single-use.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generate a reset token, use it, and try to use the same link again. It should fail. Many AI-generated flows mark the token as used after the password is changed — but not immediately on click, which opens a small window.&lt;/p&gt;


&lt;p&gt;Auth issues in production are not usually dramatic hacks. They're edge cases that a real user stumbles into — like a session that stays alive after account deletion, or an admin endpoint that returns data to any authenticated user regardless of role.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. API Routes That Think the Frontend Is the Last Line of Defense
&lt;/h2&gt;

&lt;p&gt;Vibe-coded apps often have validation that only lives in the form — in Zod schemas on the client, in form submission handlers, in UI state. The API route trusts that the frontend already checked everything.&lt;/p&gt;

&lt;p&gt;It didn't. Or rather, it did — until someone uses curl.&lt;/p&gt;

&lt;p&gt;The fix is simple: every API route that accepts user input validates that input server-side, independently.&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;// Common in AI-generated code — validation only happens in the form component&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="c1"&gt;// body.amount could be negative, null, a string, or 999999999&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCharge&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// What you want&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createChargeSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getServerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createChargeSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Now you can trust the data&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createCharge&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;parsed&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check for mass assignment too.&lt;/strong&gt; If you're doing &lt;code&gt;db.user.update({ data: body })&lt;/code&gt;, a user could pass &lt;code&gt;{ role: 'admin', stripeCustomerId: 'someone_elses_id' }&lt;/code&gt; in the request body. Never pass user-controlled data directly to a database update without explicitly picking the fields you allow.&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;// Dangerous&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Safe&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// role, email, stripeCustomerId — not here&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Error Messages Are Telling People Too Much
&lt;/h2&gt;

&lt;p&gt;In development, you want verbose errors. In production, you do not want stack traces or database errors reaching the browser.&lt;/p&gt;

&lt;p&gt;Open your app's network tab and trigger a few errors — a failed form submission, a 404 for a resource that doesn't exist, a request to an endpoint without auth. What does the response body contain?&lt;/p&gt;

&lt;p&gt;A typical AI-generated error handler:&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="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;500&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;error.message&lt;/code&gt; in a database error might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid `prisma.user.findUnique()` invocation:
column "users"."emailAddress" does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells an attacker your ORM, your schema shape, and that you have a column naming inconsistency. None of that should leave the server.&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Log the real error for yourself&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Return a safe message to the client&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Something went wrong. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check: are you calling &lt;code&gt;JSON.stringify(error)&lt;/code&gt; anywhere in response bodies? Error objects serialized to JSON can expose a lot of internal state.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Webhook Handlers That Trust Everything
&lt;/h2&gt;

&lt;p&gt;If your app uses Stripe, Clerk, Resend, GitHub, or any other service that sends webhooks, those endpoints need signature verification. Without it, anyone can POST to your webhook URL with fake events.&lt;/p&gt;

&lt;p&gt;AI tools often generate the webhook handler but skip the signature check, especially if you're prototyping quickly with Stripe CLI in local dev (which verifies automatically).&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;// Missing verification — dangerous&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;activateSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&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;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// With Stripe signature verification&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webhooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;constructEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;activateSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customer&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;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check: is your payment webhook handler idempotent? Stripe can send the same event more than once. If &lt;code&gt;activateSubscription&lt;/code&gt; charges the user again or creates a duplicate record on a second call, that's a real production bug.&lt;/p&gt;


&lt;p&gt;Every webhook handler should do two things before touching your database: verify the signature, and check whether you've already processed this event ID. Both are skippable in dev. Neither is optional in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Secrets That Ended Up in the Wrong Place
&lt;/h2&gt;

&lt;p&gt;Run this command in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--full-history&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; .env
git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--full-history&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; .env.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;.env&lt;/code&gt; or &lt;code&gt;.env.local&lt;/code&gt; ever got committed — even once, even before you added them to &lt;code&gt;.gitignore&lt;/code&gt; — those secrets are in your git history. GitHub has tooling that detects this, but you should also check manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your &lt;code&gt;.gitignore&lt;/code&gt; is actually working:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
git ls-files .env&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any &lt;code&gt;.env&lt;/code&gt; files appear in &lt;code&gt;ls-files&lt;/code&gt;, they're tracked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check for secrets in your frontend bundle.&lt;/strong&gt; If you accidentally used a server-side API key in a client component — passed it as a prop, included it in a &lt;code&gt;const&lt;/code&gt; in a shared file that got bundled — it'll be visible in the browser's JavaScript. Open DevTools, go to Sources, and search for your key name.&lt;/p&gt;

&lt;p&gt;In Next.js, any &lt;code&gt;process.env.VARIABLE&lt;/code&gt; that doesn't start with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; should never appear in client-side code. If it does, something's wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check for missing environment validation.&lt;/strong&gt; If your app starts without a required secret, what happens? Ideally it fails loudly at startup. AI-generated apps often let missing env variables surface as confusing errors at runtime — &lt;code&gt;Cannot read properties of undefined&lt;/code&gt; six layers deep.&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;// Add this to your app startup or a lib/env.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requiredVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NEXTAUTH_SECRET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;requiredVars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Missing required environment variable: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;h2&gt;
  
  
  6. Data Model Decisions That Will Hurt at 1,000 Users
&lt;/h2&gt;

&lt;p&gt;You don't need to optimize for scale before launch. You do need to avoid decisions that are expensive to undo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check for missing indexes on columns you filter or join on.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're doing &lt;code&gt;db.post.findMany({ where: { userId: session.user.id } })&lt;/code&gt;, and &lt;code&gt;userId&lt;/code&gt; doesn't have a database index, that query does a full table scan on every page load. Fine at 100 rows. Brutal at 50,000.&lt;/p&gt;

&lt;p&gt;For a Prisma schema, add indexes where you filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model Post {
  id        String   @id @default(cuid())
  userId    String
  createdAt DateTime @default(now())
  user      User     @relation(fields: [userId], references: [id])

  @@index([userId])           // add this
  @@index([userId, createdAt]) // and this if you sort by date
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check cascade behavior on deletes.&lt;/strong&gt; If you delete a user, what happens to their posts, their payment records, their audit logs? AI-generated schemas often use &lt;code&gt;onDelete: Cascade&lt;/code&gt; everywhere because it's the simple answer. Sometimes that's right. Sometimes you want &lt;code&gt;Restrict&lt;/code&gt; (block deletion until child records are removed) or &lt;code&gt;SetNull&lt;/code&gt; (preserve the records, just remove the user link).&lt;/p&gt;

&lt;p&gt;Find every &lt;code&gt;onDelete&lt;/code&gt; in your Prisma schema and verify each one matches your actual intent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check for missing &lt;code&gt;updatedAt&lt;/code&gt; timestamps.&lt;/strong&gt; You will want to know when records were last modified. Add &lt;code&gt;updatedAt DateTime @updatedAt&lt;/code&gt; to every table that isn't append-only. Adding it later requires a migration and a backfill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check for soft delete if you need it.&lt;/strong&gt; If your product involves anything users create and might want back (documents, workspaces, projects), hard deletes are risky. A &lt;code&gt;deletedAt DateTime?&lt;/code&gt; column lets you recover from accidental deletions. AI-generated apps almost never include this — they use &lt;code&gt;db.post.delete()&lt;/code&gt; everywhere.&lt;/p&gt;


&lt;p&gt;Data model mistakes compound. A missing index is a slow query now and a production incident at 10x the users. An accidental hard delete is recoverable until it isn't. Spend 30 minutes reviewing the schema before launch — it's cheaper than a midnight migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Deployment Assumptions That Break on Vercel or Railway
&lt;/h2&gt;

&lt;p&gt;Vercel and Railway are stateless by default. Your code runs in ephemeral containers that can restart, scale, or be replaced at any time. A lot of vibe-coded apps assume persistent state in ways that fail silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check for local filesystem usage.&lt;/strong&gt; If you're writing files to disk — temporary files, uploaded images, cached data — those don't survive a container restart on Vercel. Switch to object storage (Cloudflare R2, AWS S3, Vercel Blob) before you launch.&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;// This breaks on Vercel&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp/upload.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// This survives&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;put&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vercel/blob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&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;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;upload.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check for in-memory caching.&lt;/strong&gt; If you're caching data in a &lt;code&gt;Map&lt;/code&gt; or a module-level variable, that cache is per-instance and per-restart. On a platform that spins up multiple instances, each instance has its own cache with no shared state. Use Redis (Upstash is the easy path) for anything you need to cache across requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your database connection handling.&lt;/strong&gt; Serverless functions open a new connection per invocation. Without connection pooling, you'll hit your database's connection limit fast under load. For Prisma on Vercel, you need either PgBouncer or Prisma Accelerate. For Drizzle, check your connection pool settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your cold start behavior.&lt;/strong&gt; First request to a new serverless function instance can be slow — sometimes several seconds. If your auth flow or payment flow hits this, users will see an inexplicable delay on the most important interaction in your product. Test the cold path explicitly.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Rate Limiting and Abuse Prevention
&lt;/h2&gt;

&lt;p&gt;Your endpoints have no rate limiting. Every AI-generated app I've reviewed has this gap. It's not dramatic — it just means anyone can hammer your API indefinitely, trigger unlimited password reset emails, or enumerate your users by cycling through email addresses.&lt;/p&gt;

&lt;p&gt;The easiest solution in 2026 is &lt;a href="https://upstash.com/docs/redis/sdks/ratelimit-ts/overview" rel="noopener noreferrer"&gt;Upstash Ratelimit&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ratelimit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@upstash/ratelimit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@upstash/redis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ratelimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEnv&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slidingWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10 s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 10 requests per 10 seconds&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-forwarded-for&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ratelimit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;success&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Too many requests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&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;// rest of handler&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply stricter limits to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Password reset / magic link endpoints (max 3 per hour per email)&lt;/li&gt;
&lt;li&gt;Auth endpoints (max 10 per minute per IP)&lt;/li&gt;
&lt;li&gt;Any endpoint that sends email or SMS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - Any endpoint that triggers a paid action
&lt;/h2&gt;

&lt;h2&gt;
  
  
  9. The Production Basics Nobody Vibe-Coded In
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Error tracking.&lt;/strong&gt; You need to know when things break in production before your users tell you. Sentry has a generous free tier and integrates in 5 minutes with Next.js. Without it, your errors exist as silent database errors, half-completed requests, and confused users who stopped using the product.&lt;br&gt;
&lt;/p&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; @sentry/nextjs
npx @sentry/wizard@latest &lt;span class="nt"&gt;-i&lt;/span&gt; nextjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Health check endpoint.&lt;/strong&gt; If you're on Railway, Render, or any platform that monitors your app's health, you need a route that returns 200 when the app is healthy. Keep it simple:&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/health/route.ts&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="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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Logging.&lt;/strong&gt; &lt;code&gt;console.log&lt;/code&gt; in production goes... somewhere. On Vercel, it goes to the function logs. On Railway, it goes to the container logs. Make sure you know where your logs actually are and how to access them when something goes wrong at 2am. For anything beyond simple text, structured logging with something like &lt;code&gt;pino&lt;/code&gt; makes filtering and searching significantly easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email deliverability basics.&lt;/strong&gt; If your app sends email — welcome emails, password resets, notifications — check that your sending domain has SPF, DKIM, and DMARC configured. Without them, your emails go to spam. Resend's dashboard will show you if these are missing. This is a 20-minute fix that most vibe-coded apps skip.&lt;/p&gt;


&lt;p&gt;Error tracking and structured logs are the difference between "something is wrong and I don't know what" and "this API route is failing for users who signed up with a Google account." The first one takes days to debug. The second takes 20 minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 30-Minute Audit Checklist
&lt;/h2&gt;

&lt;p&gt;Work through this before you share the link more widely. Each item is a yes/no — if the answer is no, it goes in the list of things to fix this week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every API route independently verifies auth (not just middleware)&lt;/li&gt;
&lt;li&gt;[ ] Role/permission checks happen server-side, not just in the UI&lt;/li&gt;
&lt;li&gt;[ ] Deleting a session in the database actually logs the user out&lt;/li&gt;
&lt;li&gt;[ ] Password reset tokens expire and are single-use&lt;/li&gt;
&lt;li&gt;[ ] User deletion cleans up sessions properly
&lt;strong&gt;API and Input&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] API routes validate all user input server-side with Zod or equivalent&lt;/li&gt;
&lt;li&gt;[ ] Database updates explicitly pick allowed fields (no mass assignment)&lt;/li&gt;
&lt;li&gt;[ ] Production error responses don't include stack traces or schema details
&lt;strong&gt;Webhooks&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] All webhook endpoints verify the provider signature&lt;/li&gt;
&lt;li&gt;[ ] Payment webhook handlers are idempotent (safe to call twice)
&lt;strong&gt;Secrets&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;.env&lt;/code&gt; files have never been committed (check git history)&lt;/li&gt;
&lt;li&gt;[ ] No server-side secrets appear in the browser bundle&lt;/li&gt;
&lt;li&gt;[ ] App fails with a clear error at startup if required env vars are missing
&lt;strong&gt;Data Model&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Indexes exist on columns used in &lt;code&gt;where&lt;/code&gt; and &lt;code&gt;orderBy&lt;/code&gt; clauses&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;onDelete&lt;/code&gt; behavior is intentional on every relation&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;updatedAt&lt;/code&gt; timestamps exist on mutable tables&lt;/li&gt;
&lt;li&gt;[ ] Soft deletes are considered for user-created content
&lt;strong&gt;Deployment&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] No local filesystem writes (use object storage instead)&lt;/li&gt;
&lt;li&gt;[ ] No in-memory caches (use Redis for shared state)&lt;/li&gt;
&lt;li&gt;[ ] Database connection pooling is configured for serverless
&lt;strong&gt;Rate Limiting&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Auth endpoints are rate limited&lt;/li&gt;
&lt;li&gt;[ ] Email-sending endpoints are rate limited&lt;/li&gt;
&lt;li&gt;[ ] Any endpoint triggering paid actions is rate limited
&lt;strong&gt;Production Basics&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Error tracking is set up (Sentry or equivalent)&lt;/li&gt;
&lt;li&gt;[ ] Health check endpoint exists&lt;/li&gt;
&lt;li&gt;[ ] You know where your production logs live&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  - [ ] Email sending domain has SPF, DKIM, DMARC
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What to Fix First
&lt;/h2&gt;

&lt;p&gt;If everything on that list is unchecked, you have a day's work — not a week's. Most of these are 30-minute fixes.&lt;/p&gt;

&lt;p&gt;The priority order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Auth gaps&lt;/strong&gt; — these have the highest potential for user data exposure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side input validation&lt;/strong&gt; — stops the most common class of attacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook signature verification&lt;/strong&gt; — especially if payments are involved&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets audit&lt;/strong&gt; — check git history, check the browser bundle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error tracking&lt;/strong&gt; — you're flying blind without it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — before you do any marketing or sharing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data model review&lt;/strong&gt; — before you hit meaningful user numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment assumptions&lt;/strong&gt; — before you scale
The good news: most vibe-coded MVPs only have 5–8 actual issues from this list, not all of them. AI tools have gotten genuinely good at the standard patterns. The gaps are almost always in the specific edge cases — what happens when auth is missing, when a secret is wrong, when an input is unexpected.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  When to Call In Someone Else
&lt;/h2&gt;

&lt;p&gt;This audit covers the obvious gaps. It doesn't cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex multi-tenant data isolation (one tenant seeing another's data)&lt;/li&gt;
&lt;li&gt;Subtle race conditions in payment and subscription flows&lt;/li&gt;
&lt;li&gt;Performance issues that only show up at scale&lt;/li&gt;
&lt;li&gt;Security vulnerabilities in your specific business logic
If you've worked through this list and something still feels wrong — or if any of the above categories are central to your product — that's the point where a technical review from someone outside the build is worth the time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a second pair of eyes on a vibe-coded codebase before it goes to more users, &lt;a href="https://dev.to/services/production-readiness-upgrade"&gt;see Production Readiness Upgrade&lt;/a&gt;. If you want to talk through where the risks actually are first, &lt;a href="https://dev.to/book"&gt;book a 20-minute call&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Vibe-coded MVPs are a real and legitimate way to ship faster in 2026. The AI coding tools are genuinely good. The code they produce is usually fine — clean, readable, structurally sound.&lt;/p&gt;

&lt;p&gt;The gaps are almost always in the things that weren't explicitly asked for: what happens on auth failure, what happens to a secret that slipped into client code, what happens when a webhook fires twice.&lt;/p&gt;

&lt;p&gt;This audit is not about distrusting AI tools. It's about understanding where the gap between "works in development" and "ready for real users" actually lives — and closing it before someone else finds it for you.&lt;/p&gt;

&lt;p&gt;If you found something specific that broke in your vibe-coded build that isn't in this list, the &lt;a href="https://dev.to/services/production-readiness-upgrade"&gt;Production Readiness Upgrade&lt;/a&gt; is exactly the service for that kind of targeted cleanup.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cursor</category>
      <category>programming</category>
      <category>claude</category>
    </item>
    <item>
      <title>OpenAI on AWS Bedrock: The AI SaaS Provider Landscape Just Shifted</title>
      <dc:creator>Somnath Khadanga</dc:creator>
      <pubDate>Thu, 07 May 2026 16:51:48 +0000</pubDate>
      <link>https://dev.to/somnath_khadanga_2e5c2364/openai-on-aws-bedrock-the-ai-saas-provider-landscape-just-shifted-h83</link>
      <guid>https://dev.to/somnath_khadanga_2e5c2364/openai-on-aws-bedrock-the-ai-saas-provider-landscape-just-shifted-h83</guid>
      <description>&lt;p&gt;The AI provider landscape changed twice in one week.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;April 28&lt;/strong&gt;, OpenAI ended its exclusivity arrangement with Microsoft and announced expanded availability on AWS. On &lt;strong&gt;May 4&lt;/strong&gt;, AWS made it concrete: GPT-5.5 and GPT-5.4 are now available through &lt;strong&gt;Amazon Bedrock&lt;/strong&gt; in limited preview, &lt;strong&gt;Codex&lt;/strong&gt; is on Bedrock as a CLI, desktop app, and VS Code extension, and a new &lt;strong&gt;Bedrock Managed Agents&lt;/strong&gt; product wraps OpenAI's frontier models with AWS infrastructure for production agent workflows.&lt;/p&gt;

&lt;p&gt;That's the news cycle. Here's the founder version: if you've been building an AI SaaS on the assumption that "OpenAI = Azure" and "Anthropic = AWS" — those defaults no longer hold. Both frontier providers now sit on AWS. Both also sit elsewhere. Your provider decisions just got more interesting and harder.&lt;/p&gt;

&lt;p&gt;This post is about how to think through that, not which "winner" to pick.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p&amp;gt;GPT-5.5 and GPT-5.4 are now on AWS Bedrock alongside Anthropic Claude — meaning AWS hosts both frontier providers in one place for the first time.&amp;lt;/p&amp;gt;



&amp;lt;p&amp;gt;Codex on Bedrock means you can use OpenAI's coding agent inside an AWS account using AWS auth and AWS billing — instead of separate OpenAI API keys.&amp;lt;/p&amp;gt;



&amp;lt;p&amp;gt;For most existing SaaS apps, this is more about future flexibility than an immediate "switch providers" decision.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Actually Got Announced
&lt;/h2&gt;

&lt;p&gt;Three things, in plain language:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI models on Amazon Bedrock (Limited preview).&lt;/strong&gt; GPT-5.5 and GPT-5.4 — OpenAI's frontier models — are now callable through the Bedrock API with the same patterns you'd use for Anthropic Claude or Meta Llama. Same IAM auth, same VPC endpoints, same CloudWatch metrics, same Bedrock pricing model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codex on Amazon Bedrock (Limited preview).&lt;/strong&gt; OpenAI's coding agent — the same one that ships as &lt;code&gt;codex&lt;/code&gt; CLI, a Codex desktop app, and a VS Code extension — can now run against Bedrock-hosted OpenAI models. For teams already inside an AWS environment, this means coding-agent traffic stays inside your AWS account boundary instead of going to a separate OpenAI account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Bedrock Managed Agents (Limited preview).&lt;/strong&gt; A new managed service that wraps OpenAI frontier models in AWS-hosted agent infrastructure. Memory, tool use, retrieval, evaluation — all the agent plumbing — managed by AWS instead of built by you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three are limited preview. Most teams won't have access on day one. The mid-term direction is clear: AWS becomes a place where you can use any major frontier model under one billing relationship, one IAM model, and one set of compliance certifications.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters for AI SaaS Founders
&lt;/h2&gt;

&lt;p&gt;Until April, the practical picture for founders building AI features looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI&lt;/strong&gt; = direct API or Azure OpenAI Service. Different auth, different SLAs, different regions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic&lt;/strong&gt; = direct API or AWS Bedrock or Google Vertex AI. Easy to use on Bedrock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Gemini&lt;/strong&gt; = direct API or Vertex AI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Llama&lt;/strong&gt; = AWS Bedrock or Vertex AI or self-hosted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you wanted to be multi-provider — calling OpenAI for one feature and Claude for another — you were managing at least two billing relationships, two auth flows, and two sets of SDKs. Most founders didn't bother. They picked one provider and stuck with it.&lt;/p&gt;

&lt;p&gt;Bedrock now collapses that picture. Both OpenAI and Anthropic models live behind the same API surface. Switching between them — or A/B testing them on a per-feature basis — becomes a config change instead of a rewrite.&lt;/p&gt;

&lt;p&gt;That sounds like a clean win, and for new builds it largely is. For &lt;strong&gt;existing&lt;/strong&gt; SaaS apps, the calculation is more nuanced. The cost of changing your stack is real, and the benefit of "now I can switch" is mostly latent until you actually need to switch.&lt;/p&gt;

&lt;p&gt;If your SaaS is already running on OpenAI direct API or Anthropic direct API and it's working, the right answer this week is probably: don't move yet. Watch for general availability, watch for pricing parity, then revisit.&lt;/p&gt;

&lt;p&gt;If you're picking a stack for a new product right now, that's where this changes things.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Provider Landscape in May 2026
&lt;/h2&gt;

&lt;p&gt;Here's how I'd map it out today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Direct API&lt;/th&gt;
&lt;th&gt;AWS Bedrock&lt;/th&gt;
&lt;th&gt;Azure&lt;/th&gt;
&lt;th&gt;Google Vertex&lt;/th&gt;
&lt;th&gt;Self-host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenAI (GPT-5.x, Codex)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;🟡 limited preview&lt;/td&gt;
&lt;td&gt;✅ Azure OpenAI&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Anthropic (Claude)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Gemini&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Meta Llama&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mistral / Cohere / others&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ each&lt;/td&gt;
&lt;td&gt;✅ Bedrock&lt;/td&gt;
&lt;td&gt;varies&lt;/td&gt;
&lt;td&gt;varies&lt;/td&gt;
&lt;td&gt;varies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The headline: &lt;strong&gt;AWS Bedrock is now the only managed surface that hosts both OpenAI and Anthropic frontier models&lt;/strong&gt;. Direct API from each provider is still the cheapest and lowest-latency path, but Bedrock is the only "neutral ground" for multi-provider architectures.&lt;/p&gt;

&lt;p&gt;Anthropic's relationship with AWS is also worth flagging. In April, Amazon committed to investing &lt;strong&gt;up to $25B more&lt;/strong&gt; in Anthropic, and Anthropic pledged &lt;strong&gt;$100B in cloud spending&lt;/strong&gt; to AWS over time. That alignment isn't going away. Bedrock's bias toward Anthropic models — better region coverage, deeper integrations, longer-running availability — is structural.&lt;/p&gt;

&lt;p&gt;OpenAI on Bedrock is real but newer and smaller. Treat that as the asymmetry it is.&lt;/p&gt;


&lt;p&gt;For most SaaS founders, the practical takeaway from this map isn't "go multi-provider." It's "the cost of going multi-provider later is now lower than it used to be." That's a different kind of optionality.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five Real Decisions This Forces You to Think About
&lt;/h2&gt;

&lt;p&gt;If you're building or scaling an AI SaaS product, here are the actual questions worth a meeting:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Direct API or Bedrock?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Direct API wins on:&lt;/strong&gt; lowest latency, lowest per-token cost, fastest access to new models (Bedrock typically lags by weeks to months on new model releases), simpler SDKs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bedrock wins on:&lt;/strong&gt; unified billing under your AWS account, IAM-based auth (no API keys to manage), VPC isolation, CloudWatch monitoring out of the box, easier procurement for enterprise customers (your buyer doesn't have to onboard OpenAI as a separate vendor), shared infrastructure with the rest of your AWS workload.&lt;/p&gt;

&lt;p&gt;For a solo founder or small team building a B2C product, direct API is almost always the right starting answer. For a B2B SaaS selling to enterprise — especially anyone in regulated industries — Bedrock removes a real procurement headache.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Are you ever going to actually be multi-provider?
&lt;/h3&gt;

&lt;p&gt;A lot of teams say "we want optionality" without ever exercising it. Multi-provider architectures cost something — abstraction layers, extra testing, prompt drift between models, more runtime configuration.&lt;/p&gt;

&lt;p&gt;Honest test: if your current provider had a 4-hour outage tomorrow, would you actually fail over to a different model, or would you just wait? If the answer is "wait" then you're not really multi-provider — you're paying the abstraction cost without getting the benefit.&lt;/p&gt;

&lt;p&gt;If the answer is "fail over" then you should have built that path already, in which case Bedrock's unified API is genuinely useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Where do your customer's tokens live?
&lt;/h3&gt;

&lt;p&gt;For B2B AI SaaS, your customer is increasingly going to ask: "where does my data go when I use your AI features, and who has it?"&lt;/p&gt;

&lt;p&gt;Direct OpenAI API answer: "It goes to OpenAI servers. They have a data processing agreement we signed."&lt;/p&gt;

&lt;p&gt;Bedrock answer: "It goes through your AWS account, which is already covered under your AWS BAA / DPA / [whichever framework]. AWS doesn't train on it. Neither does OpenAI through this surface."&lt;/p&gt;

&lt;p&gt;Some enterprise customers care a lot about which answer they hear. Some don't care at all. Know which kind of customer you're selling to.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. What's your latency budget?
&lt;/h3&gt;

&lt;p&gt;Bedrock adds a hop. Calls go through AWS's regional Bedrock endpoint, which routes to the model provider's infrastructure. In practice this typically adds 50–200ms compared to direct API, depending on region.&lt;/p&gt;

&lt;p&gt;For most SaaS workloads — chat features, summarization, search — that's invisible. For latency-critical features (real-time autocomplete, voice, agent loops with tight cycle times), it matters. If your AI feature lives in the user's hot path and the perceived speed is already a complaint, this is a real concern. If it's covered by a loading spinner anyway, it isn't.&lt;/p&gt;

&lt;p&gt;This connects to the broader "your Next.js app feels slow after launch" pattern — the kind of work covered in &lt;a href="https://dev.to/services/nextjs-performance-optimization"&gt;Next.js Performance Optimization&lt;/a&gt;. AI latency is now part of total user-perceived latency, and it's easy to underestimate.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Are you using an AI Gateway?
&lt;/h3&gt;

&lt;p&gt;The third option that doesn't show up in most "Bedrock vs direct API" debates: &lt;strong&gt;AI gateways&lt;/strong&gt; — Vercel AI Gateway, OpenRouter, Portkey, Helicone. These sit between your app and any frontier provider, giving you observability, rate limiting, automatic provider failover, and a unified API across providers.&lt;/p&gt;

&lt;p&gt;For solo founders and small teams, an AI gateway is often the better answer than either direct or Bedrock. You get most of the operational benefits of Bedrock without the AWS lock-in, and you get easy provider switching without writing your own abstraction layer.&lt;/p&gt;

&lt;p&gt;If you're already on Vercel, the AI Gateway is the lowest-friction path. If you're already deep into AWS, Bedrock makes more sense.&lt;/p&gt;


&lt;p&gt;The "right" answer is rarely "use the most powerful platform." It's "use the platform whose lock-in you're least worried about, given the way your product will actually evolve over the next 12 months."&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Wouldn't Do Yet
&lt;/h2&gt;

&lt;p&gt;A few things the news cycle is pushing that I'd push back on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't migrate a working stack just because Bedrock has OpenAI now.&lt;/strong&gt; If your SaaS already runs on OpenAI direct API and your billing, latency, and customer compliance are all fine, the cost of moving to Bedrock is real and the benefit is largely "future optionality." Migrate when you have a concrete reason to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't bet on Codex on Bedrock for production yet.&lt;/strong&gt; Codex CLI is a developer-experience tool, not a customer-facing API. The Bedrock version is limited preview. Use it for your own engineering workflow if you want, but don't build customer features that depend on it being generally available on a particular timeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't go multi-provider before you have one provider working well.&lt;/strong&gt; Multi-provider abstractions are expensive to maintain and easy to over-engineer. Ship the single-provider version first. Add the abstraction the first time you have a real reason to switch — and not before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't trust pricing comparisons that don't include egress and gateway costs.&lt;/strong&gt; Bedrock's per-token pricing isn't the full picture. Your AWS networking costs, log storage costs, and Bedrock-specific markups all factor in. The honest comparison is your monthly AWS bill before vs after, not the rate card.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;Two patterns are clear from this week's announcements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The frontier-model business is consolidating around three hyperscalers&lt;/strong&gt; — AWS, Azure, GCP — even while staying nominally multi-provider. The economic gravity is pulling everything toward managed surfaces. If you've been holding off on cloud commitments to "stay neutral," that's getting harder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud-provider lock-in for AI SaaS is becoming structural.&lt;/strong&gt; Once your AI traffic, your auth, your monitoring, and your customer's data residency commitments all live inside one AWS account, switching is no longer a code change. It's a compliance change.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn't necessarily bad. Concentration trades away some flexibility for a lot of operational simplicity, and for B2B SaaS especially, that's often the right trade. But it's worth making the trade consciously, not by default.&lt;/p&gt;

&lt;p&gt;If you're picking a stack for a new AI SaaS right now, the questions worth answering first aren't "GPT-5.5 or Claude?" — they're:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where is your customer's data going to live?&lt;/li&gt;
&lt;li&gt;What does your auth and billing look like in 12 months when you have 10 features instead of 1?&lt;/li&gt;
&lt;li&gt;Is multi-provider real for your product, or aspirational?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions matter more than the model picker. They're also exactly the kinds of decisions covered by &lt;a href="https://dev.to/services/saas-mvp-development"&gt;SaaS MVP Development&lt;/a&gt; when the AI feature is the product, and &lt;a href="https://dev.to/services/ai-saas-development"&gt;AI SaaS Development&lt;/a&gt; when AI is one feature inside a broader product.&lt;/p&gt;


&lt;p&gt;The teams I see make the best AI provider decisions are the ones that decide what they're optimizing for first — latency, cost, compliance, optionality — and then pick the stack that fits. The teams that make the worst decisions pick the stack first and figure out the tradeoffs later.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  Is OpenAI available on AWS Bedrock?
&lt;/h3&gt;

&lt;p&gt;Yes, as of May 2026 — GPT-5.5 and GPT-5.4 are available through Amazon Bedrock in limited preview. Codex is also on Bedrock as a CLI, desktop app, and VS Code extension. General availability hasn't been confirmed yet, so production builds should plan for a waiting period.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is AWS Bedrock cheaper than using the OpenAI API directly?
&lt;/h3&gt;

&lt;p&gt;Not necessarily. Bedrock adds its own markup on top of the provider's base rate, plus you pay for AWS networking, CloudWatch logging, and Bedrock-specific costs. Direct API is usually cheaper for pure token cost. Bedrock's value is in the operational layer — unified billing, IAM auth, VPC isolation, compliance — not in per-token savings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use both OpenAI and Anthropic Claude on the same Bedrock account?
&lt;/h3&gt;

&lt;p&gt;Yes. That's the main structural change from this announcement. Before May 2026, Bedrock hosted Anthropic, Meta, Mistral, and others — but not OpenAI. Now both major frontier providers (OpenAI and Anthropic) are callable through the same Bedrock API surface, with the same auth and billing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does AWS Bedrock support GPT-4?
&lt;/h3&gt;

&lt;p&gt;Not currently. The models announced are GPT-5.5 and GPT-5.4, which are OpenAI's frontier-tier models as of 2026. Older GPT-4 variants aren't listed in the Bedrock catalog. For GPT-4-class workloads, the direct OpenAI API or Azure OpenAI Service remain the current paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does OpenAI on Bedrock compare to Azure OpenAI Service?
&lt;/h3&gt;

&lt;p&gt;Azure OpenAI has been available since 2023 and has broader model coverage and more enterprise deployment options. Bedrock's OpenAI offering is newer and in limited preview. If you're already on Azure or have an existing Azure OpenAI deployment, there's no reason to move. If you're already AWS-native, Bedrock is now a viable alternative to keep your AI traffic inside the same cloud account.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;OpenAI on Bedrock is a real shift. It changes what's possible, especially for B2B SaaS selling into enterprise. It doesn't change what's necessary for most existing AI SaaS apps that are already shipping.&lt;/p&gt;

&lt;p&gt;The right move this week is mostly "watch and read." The right move in three months — when limited preview turns into general availability, when pricing settles, when the integrations mature — will be more concrete.&lt;/p&gt;

&lt;p&gt;If you're earlier in the journey and you haven't picked your provider yet, this is the moment to think harder about it than usual. The default of "use OpenAI direct" or "use Anthropic direct" is still defensible. But the calculus around Bedrock and AI gateways is shifting fast enough that the answer that was right last quarter may not be right next quarter.&lt;/p&gt;

&lt;p&gt;Posts like &lt;a href="https://dev.to/blog/which-ai-features-are-worth-building-in-saas"&gt;Which AI Features Are Actually Worth Building in a SaaS Product Right Now&lt;/a&gt; cover the feature side of this question — what to build. This post covers the infrastructure side — where to build it. They go together.&lt;/p&gt;

&lt;p&gt;If you'd rather have a single call to figure out what your specific SaaS actually needs first — direct API, Bedrock, or AI gateway — that's exactly what &lt;a href="https://dev.to/services/ai-saas-development"&gt;AI SaaS Development&lt;/a&gt; covers, and &lt;a href="https://dev.to/book"&gt;a 20-minute strategy call&lt;/a&gt; is usually enough to sort the first few decisions.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>aws</category>
      <category>openai</category>
    </item>
    <item>
      <title>What the Vercel Security Incident Should Teach SaaS Teams About Production Readiness</title>
      <dc:creator>Somnath Khadanga</dc:creator>
      <pubDate>Sat, 25 Apr 2026 07:44:24 +0000</pubDate>
      <link>https://dev.to/somnath_khadanga_2e5c2364/what-the-vercel-security-incident-should-teach-saas-teams-about-production-readiness-594e</link>
      <guid>https://dev.to/somnath_khadanga_2e5c2364/what-the-vercel-security-incident-should-teach-saas-teams-about-production-readiness-594e</guid>
      <description>&lt;p&gt;A lot of teams think production readiness is mostly about uptime, performance, deployment speed, and bug rates.&lt;/p&gt;

&lt;p&gt;That is incomplete.&lt;/p&gt;

&lt;p&gt;As of &lt;a href="https://vercel.com/kb/bulletin/vercel-april-2026-security-incident" rel="noopener noreferrer"&gt;Vercel's April 21, 2026 security bulletin update&lt;/a&gt;, the company says attackers gained unauthorized access to certain internal systems, impacted a limited subset of customers, and traced the incident to a compromise of Context.ai, a third-party AI tool used by a Vercel employee. Vercel says the attack path involved the employee's Google Workspace account and exposure of environment variables that were not marked as sensitive.&lt;/p&gt;

&lt;p&gt;That is why this is not just a "Vercel got breached" story.&lt;/p&gt;

&lt;p&gt;It is a reminder that production readiness also includes workflow security: how your team connects third-party tools, how OAuth access is handled, how credentials are stored, and how much internal access can be exposed when one trusted integration goes wrong.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p&amp;gt;Production readiness includes workflow security, not just release reliability.&amp;lt;/p&amp;gt;



&amp;lt;p&amp;gt;OAuth access and secret hygiene can become incident paths if they are not reviewed aggressively.&amp;lt;/p&amp;gt;



&amp;lt;p&amp;gt;A compromise in one trusted tool can expand into a larger operational blast radius very quickly.&amp;lt;/p&amp;gt;



&amp;lt;p&amp;gt;Mature teams need incident playbooks for vendor and integration failures, not just app bugs.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;In this post, I will focus on the real lesson for SaaS teams: production readiness is not only about the code you ship. It is also about the workflows around the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happened
&lt;/h2&gt;

&lt;p&gt;On April 19, 2026, Vercel disclosed that attackers had gained unauthorized access to certain internal systems and that the incident affected a limited subset of customers.&lt;/p&gt;

&lt;p&gt;By April 21, Vercel had added more detail to its bulletin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The incident originated from a compromise of Context.ai, a third-party AI tool&lt;/li&gt;
&lt;li&gt;The attack path involved a Vercel employee's Google Workspace account&lt;/li&gt;
&lt;li&gt;Some environment variables that were not marked as sensitive should be treated as potentially exposed&lt;/li&gt;
&lt;li&gt;Customers were advised to review activity logs, inspect recent deployments, rotate exposed secrets, and enable stronger account protections&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Independent reporting from &lt;a href="https://techcrunch.com/2026/04/20/app-host-vercel-confirms-security-incident-says-customer-data-was-stolen-via-breach-at-context-ai/" rel="noopener noreferrer"&gt;TechCrunch&lt;/a&gt; and &lt;a href="https://www.theverge.com/tech/914723/vercel-hacked" rel="noopener noreferrer"&gt;The Verge&lt;/a&gt; filled in some of the surrounding context, but the key operational lesson was already visible in Vercel's own bulletin: a trusted workflow connection became a path into more sensitive internal systems.&lt;/p&gt;

&lt;p&gt;That is the part SaaS teams should pay attention to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters Beyond Vercel
&lt;/h2&gt;

&lt;p&gt;This is not only a platform story. It is a modern engineering workflow story.&lt;/p&gt;

&lt;p&gt;Most SaaS teams now run through a web of connected systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud platforms&lt;/li&gt;
&lt;li&gt;CI/CD tools&lt;/li&gt;
&lt;li&gt;Collaboration suites&lt;/li&gt;
&lt;li&gt;AI tools&lt;/li&gt;
&lt;li&gt;OAuth-based integrations&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Internal dashboards&lt;/li&gt;
&lt;li&gt;Admin and support workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When those systems are tightly connected, a compromise in one trusted workflow can become a path into something much more important.&lt;/p&gt;

&lt;p&gt;That is why Vercel's warning about a broader compromise of the Google Workspace OAuth app matters even if you are not a Vercel customer. The pattern is bigger than the vendor.&lt;/p&gt;

&lt;p&gt;If your team uses third-party AI tools, Google Workspace integrations, deployment platforms, or shared operational credentials, the same category of weakness can exist in your stack too.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Lesson: Production Readiness Includes Workflow Security
&lt;/h2&gt;

&lt;p&gt;Many teams still treat workflow security as an internal IT concern instead of a product concern.&lt;/p&gt;

&lt;p&gt;I think that is a mistake.&lt;/p&gt;

&lt;p&gt;If your product depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy pipelines&lt;/li&gt;
&lt;li&gt;Hosted infrastructure&lt;/li&gt;
&lt;li&gt;Admin dashboards&lt;/li&gt;
&lt;li&gt;OAuth-connected tools&lt;/li&gt;
&lt;li&gt;Environment variables&lt;/li&gt;
&lt;li&gt;Support workflows&lt;/li&gt;
&lt;li&gt;Build and release systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then workflow security directly affects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Release confidence&lt;/li&gt;
&lt;li&gt;Operational reliability&lt;/li&gt;
&lt;li&gt;Incident response speed&lt;/li&gt;
&lt;li&gt;Customer trust&lt;/li&gt;
&lt;li&gt;Engineering velocity after an incident&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I would treat this as a production-readiness issue, not just a one-off security headline.&lt;/p&gt;

&lt;p&gt;This is also the same reason I map this kind of work more naturally to &lt;a href="https://dev.to/services/production-readiness-upgrade"&gt;Production Readiness Upgrade&lt;/a&gt; than to a generic "security news" take. If the operating workflow around a live product is weak, the product is not actually production-ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  What SaaS Teams Should Review Immediately
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Third-Party OAuth Access
&lt;/h3&gt;

&lt;p&gt;If a third-party app connected through Google Workspace or another identity provider can become a path into internal systems, that access needs a much higher bar than "it helps productivity."&lt;/p&gt;

&lt;p&gt;I would review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which third-party apps have OAuth access to corporate accounts&lt;/li&gt;
&lt;li&gt;Which employees approved them&lt;/li&gt;
&lt;li&gt;What scopes they received&lt;/li&gt;
&lt;li&gt;Whether those tools are still actively needed&lt;/li&gt;
&lt;li&gt;Whether access review is periodic or effectively forgotten&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the most direct lesson from the Vercel incident, because the company's own April 21 bulletin tied the origin to a compromised Google Workspace OAuth app from a third-party AI tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Internal Access Blast Radius
&lt;/h3&gt;

&lt;p&gt;A compromise is bad. A compromise with broad internal reach is worse.&lt;/p&gt;

&lt;p&gt;Teams should ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If one employee account is taken over, what internal systems become reachable?&lt;/li&gt;
&lt;li&gt;What secrets, dashboards, or workflows are exposed from there?&lt;/li&gt;
&lt;li&gt;Are there internal systems that should be segmented more tightly?&lt;/li&gt;
&lt;li&gt;Does one identity unlock too much?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where product engineering, identity management, and operations stop being separate conversations.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Secret and Environment Variable Hygiene
&lt;/h3&gt;

&lt;p&gt;Vercel explicitly advised customers to review and rotate environment variables that were not marked as sensitive. That is a strong reminder that secrets hygiene is not boilerplate policy. It is a real incident-response step.&lt;/p&gt;

&lt;p&gt;For a SaaS team, I would review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where secrets are stored&lt;/li&gt;
&lt;li&gt;Who can view them&lt;/li&gt;
&lt;li&gt;How often they are rotated&lt;/li&gt;
&lt;li&gt;Which ones are over-privileged&lt;/li&gt;
&lt;li&gt;Whether urgent rotation has an actual documented playbook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vercel also shipped product changes after the incident, including making environment variable creation default to sensitive. That is a good example of a company tightening the product after learning where customers are vulnerable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Audit Visibility and Ownership
&lt;/h3&gt;

&lt;p&gt;An activity log only helps if someone actually knows when and how to use it.&lt;/p&gt;

&lt;p&gt;Vercel told customers to review activity logs and inspect recent deployments. For most SaaS teams, the question is not just "Do we have logs?" It is "Who owns checking them when something unusual happens?"&lt;/p&gt;

&lt;p&gt;I would want clear answers to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which logs matter first during a security event&lt;/li&gt;
&lt;li&gt;Who is responsible for triage&lt;/li&gt;
&lt;li&gt;How suspicious deployments are reviewed&lt;/li&gt;
&lt;li&gt;How quickly a team can decide whether to rotate secrets or disable access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An alerting system with no owner is not a security process.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Vendor Dependence and Response Discipline
&lt;/h3&gt;

&lt;p&gt;This incident is also a reminder that a hosted platform can be excellent and still become part of your risk surface.&lt;/p&gt;

&lt;p&gt;That does not mean "do not use platforms."&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Understand what access flows exist&lt;/li&gt;
&lt;li&gt;Understand what secrets live there&lt;/li&gt;
&lt;li&gt;Understand what you would rotate first if something upstream went wrong&lt;/li&gt;
&lt;li&gt;Understand how your own incident process depends on vendor communications&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To Vercel's credit, the company published a customer bulletin, indicators of compromise, mitigation guidance, and product hardening updates quickly. That is what a mature vendor should do.&lt;/p&gt;

&lt;p&gt;But your team still needs its own response discipline on top of vendor response.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Founder Angle
&lt;/h2&gt;

&lt;p&gt;This kind of risk is easy for founders to underestimate because it does not show up in a demo.&lt;/p&gt;

&lt;p&gt;Users do not see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth app sprawl&lt;/li&gt;
&lt;li&gt;Weak secret rotation practices&lt;/li&gt;
&lt;li&gt;Broad internal-access paths&lt;/li&gt;
&lt;li&gt;Poor third-party review habits&lt;/li&gt;
&lt;li&gt;Unclear incident ownership&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But those things still shape how resilient the business really is.&lt;/p&gt;

&lt;p&gt;When a security incident hits, the damage is not only technical. It affects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delivery confidence&lt;/li&gt;
&lt;li&gt;Support load&lt;/li&gt;
&lt;li&gt;Customer communication&lt;/li&gt;
&lt;li&gt;Team focus&lt;/li&gt;
&lt;li&gt;Roadmap disruption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why production readiness is not only about whether the app deploys cleanly. It is also about whether the team behind the app operates securely enough to absorb risk without chaos.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Fix This Week
&lt;/h2&gt;

&lt;p&gt;If I were advising a SaaS team right now, I would start with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit all Google Workspace and identity-provider OAuth apps&lt;/li&gt;
&lt;li&gt;Remove unused or weakly justified third-party access&lt;/li&gt;
&lt;li&gt;Review which internal systems are reachable from compromised employee accounts&lt;/li&gt;
&lt;li&gt;Rotate high-blast-radius environment variables and tokens&lt;/li&gt;
&lt;li&gt;Define who owns logs, alerts, and emergency credential rotation&lt;/li&gt;
&lt;li&gt;Document a short incident checklist for third-party platform compromises&lt;/li&gt;
&lt;/ul&gt;


&lt;p&gt;These are not theoretical improvements. They are practical workflow-hardening steps that reduce the damage a compromised integration or employee account can cause.&lt;/p&gt;

&lt;p&gt;If you are also tightening package publishing and dependency workflow risk, &lt;a href="https://dev.to/blog/recent-npm-security-changes-what-saas-teams-should-fix-right-now"&gt;Recent npm Security Changes: What SaaS Teams Should Fix Right Now&lt;/a&gt; covers the package-side version of the same maturity problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The Vercel incident is not just a story about one platform having a bad week.&lt;/p&gt;

&lt;p&gt;It is a reminder that in 2026, production readiness includes more than code quality and deployment speed.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Third-party access review&lt;/li&gt;
&lt;li&gt;OAuth hygiene&lt;/li&gt;
&lt;li&gt;Secret management&lt;/li&gt;
&lt;li&gt;Internal access boundaries&lt;/li&gt;
&lt;li&gt;Clear operational ownership when something trusted stops being trustworthy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the maturity bar SaaS teams need as their products grow.&lt;/p&gt;

&lt;p&gt;If your product is live and the workflow behind it needs cleanup, this is exactly the kind of work covered in &lt;a href="https://dev.to/services/production-readiness-upgrade"&gt;Production Readiness Upgrade&lt;/a&gt;.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vercel.com/kb/bulletin/vercel-april-2026-security-incident" rel="noopener noreferrer"&gt;Vercel: April 2026 security incident bulletin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://techcrunch.com/2026/04/20/app-host-vercel-confirms-security-incident-says-customer-data-was-stolen-via-breach-at-context-ai/" rel="noopener noreferrer"&gt;TechCrunch: App host Vercel says it was hacked and customer data stolen&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.theverge.com/tech/914723/vercel-hacked" rel="noopener noreferrer"&gt;The Verge: Cloud development platform Vercel was hacked&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>vertexai</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Next.js 16 App Router Caching Changed — Here's What to Update in Your SaaS</title>
      <dc:creator>Somnath Khadanga</dc:creator>
      <pubDate>Wed, 22 Apr 2026 10:46:21 +0000</pubDate>
      <link>https://dev.to/somnath_khadanga_2e5c2364/nextjs-16-app-router-caching-changed-heres-what-to-update-in-your-saas-33e9</link>
      <guid>https://dev.to/somnath_khadanga_2e5c2364/nextjs-16-app-router-caching-changed-heres-what-to-update-in-your-saas-33e9</guid>
      <description>&lt;p&gt;If your SaaS product is on Next.js 16 or you're planning the upgrade, the biggest practical change is not a new feature — it's caching.&lt;/p&gt;

&lt;p&gt;The App Router's caching model has shifted from "cache by default, opt out when you need fresh data" to "fetch fresh by default, opt in to caching when you want it." For a lot of SaaS teams, that flipped assumption is the difference between a smooth upgrade and a week of weird bugs.&lt;/p&gt;

&lt;p&gt;This post is the shortest useful version of what changed, why it matters for SaaS apps specifically, and what I'd actually update in a live product.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;p&amp;gt;The default caching behavior in Next.js 16 is more predictable, but it shifts the burden of caching decisions to you.&amp;lt;/p&amp;gt;



&amp;lt;p&amp;gt;Many SaaS apps upgrading from 14 or 15 will see more database and API calls after upgrading unless they opt in to caching explicitly.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Short Version
&lt;/h2&gt;

&lt;p&gt;Before Next.js 16, a lot of things were cached implicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fetch()&lt;/code&gt; calls were cached unless you passed &lt;code&gt;cache: 'no-store'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Route segments were statically rendered by default&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET&lt;/code&gt; route handlers were cached&lt;/li&gt;
&lt;li&gt;The full route cache was aggressive about reusing previous renders&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Next.js 16, the model is inverted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data fetches are fresh by default&lt;/li&gt;
&lt;li&gt;Caching is something you ask for, usually with &lt;code&gt;'use cache'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Route segments are dynamic unless you explicitly mark them cacheable&lt;/li&gt;
&lt;li&gt;Revalidation is clearer and more explicit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not that one model is better than the other in the abstract. The point is that your app was built around specific assumptions, and most of those assumptions just changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters for SaaS Apps Specifically
&lt;/h2&gt;

&lt;p&gt;Marketing sites mostly did not notice this change. SaaS apps did.&lt;/p&gt;

&lt;p&gt;Here is why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SaaS apps have dashboards that mix fresh data (live metrics) with stable data (user profiles, settings)&lt;/li&gt;
&lt;li&gt;SaaS apps often had &lt;code&gt;fetch()&lt;/code&gt; calls to internal APIs that were being cached without anyone realizing&lt;/li&gt;
&lt;li&gt;SaaS apps use role-based views where the same route renders different content per user, which interacts with caching in non-obvious ways&lt;/li&gt;
&lt;li&gt;SaaS apps have admin panels where stale data is actively harmful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your dashboard suddenly feels slower after upgrading to 16, you probably lost some implicit caching you did not know you had. If your dashboard suddenly shows correct-but-old data, you almost certainly did.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Changed, With Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;fetch()&lt;/code&gt; Is No Longer Cached by Default
&lt;/h3&gt;

&lt;p&gt;Before, this was cached indefinitely unless you told it otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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.example.com/products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Next.js 16, the same call hits the network every request. If you want it cached, you now opt in explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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.example.com/products&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;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;force-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to update:&lt;/strong&gt; audit every &lt;code&gt;fetch()&lt;/code&gt; call in your &lt;code&gt;app/&lt;/code&gt; directory. Ones that pull stable data (catalogs, configurations, public content) should get explicit caching. Ones that pull per-user data should stay fresh.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;'use cache'&lt;/code&gt; Is the New Primary Caching Primitive
&lt;/h3&gt;

&lt;p&gt;Instead of caching decisions being scattered across &lt;code&gt;fetch&lt;/code&gt; options, &lt;code&gt;unstable_cache&lt;/code&gt;, and route config, Next.js 16 leans on a single directive:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getFeaturedProducts&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;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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM products WHERE featured = true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can put &lt;code&gt;'use cache'&lt;/code&gt; at the top of a file, a function, or a component. It tells the framework "everything this function returns is cacheable." Combined with &lt;code&gt;cacheLife&lt;/code&gt; and &lt;code&gt;cacheTag&lt;/code&gt;, you get explicit control over what's cached, how long, and how to invalidate it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to update:&lt;/strong&gt; anywhere you were using &lt;code&gt;unstable_cache&lt;/code&gt;, migrate to &lt;code&gt;'use cache'&lt;/code&gt;. The API is cleaner and it's no longer unstable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Route Segments Default to Dynamic
&lt;/h3&gt;

&lt;p&gt;Previously, a page was static unless something in it forced dynamic rendering. Now, the default is dynamic unless you mark the segment cacheable.&lt;/p&gt;

&lt;p&gt;For most SaaS dashboards, this matches your intent — you wanted fresh per-request data anyway. But marketing pages, docs, and public pages under &lt;code&gt;app/&lt;/code&gt; may need explicit caching to perform well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to update:&lt;/strong&gt; for each top-level route under &lt;code&gt;app/&lt;/code&gt;, decide: is this page the same for everyone, or is it per-user? If it's the same for everyone, add &lt;code&gt;'use cache'&lt;/code&gt; or set the appropriate segment config.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;GET&lt;/code&gt; Route Handlers Are No Longer Cached
&lt;/h3&gt;

&lt;p&gt;This is the one that surprised the most teams.&lt;/p&gt;

&lt;p&gt;Before, a &lt;code&gt;GET&lt;/code&gt; handler in &lt;code&gt;app/api/something/route.ts&lt;/code&gt; was cached by default if it didn't use dynamic features. Now it isn't.&lt;/p&gt;

&lt;p&gt;If you had public API endpoints that were "basically free" because Next.js was caching them at the edge without you asking — those are now hitting your database on every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to update:&lt;/strong&gt; for public, read-heavy API routes, explicitly cache them. For authenticated SaaS routes, this was probably what you wanted anyway, but check the ones you assumed were always fresh.&lt;/p&gt;


&lt;p&gt;The most common Next.js 16 upgrade bug I see in SaaS apps is a silent increase in database load. Your code did not change, but the caching that used to hide N+1 queries went away.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Upgrade Audit I'd Actually Run
&lt;/h2&gt;

&lt;p&gt;If I were upgrading a real SaaS product to Next.js 16, here's the order I'd do things in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Measure Before You Upgrade
&lt;/h3&gt;

&lt;p&gt;Record the basics before touching anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average dashboard load time (p50 and p95)&lt;/li&gt;
&lt;li&gt;Database queries per minute at steady state&lt;/li&gt;
&lt;li&gt;API route response times&lt;/li&gt;
&lt;li&gt;Any external API calls you make per request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you a baseline. Without it, "it feels slower" is just a feeling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Upgrade on a Branch, Don't Change Caching Yet
&lt;/h3&gt;

&lt;p&gt;Do the Next.js 16 upgrade with no caching changes. Run your app. What you see is the "nothing opted in" baseline — this is what your code behaves like now that the defaults flipped.&lt;/p&gt;

&lt;p&gt;Expect: more DB calls, slower dashboards, and possibly some pages that suddenly show correct data they weren't showing before.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Categorize Every Data Access Point
&lt;/h3&gt;

&lt;p&gt;Go through every &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;await db.query&lt;/code&gt;, and external API call in your &lt;code&gt;app/&lt;/code&gt; directory. For each one, decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stable and public:&lt;/strong&gt; catalogs, marketing copy, public product data. Add explicit caching with a long TTL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable and private:&lt;/strong&gt; user settings, org configurations. Cache per-user with a short to medium TTL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh and private:&lt;/strong&gt; live dashboard metrics, notifications, inbox. Do not cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh and public:&lt;/strong&gt; live leaderboards, pricing. Cache with a short TTL (seconds).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more work than it sounds, but it is one-time work. Once you've done it, your caching is explicit and debuggable forever after.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Use &lt;code&gt;cacheTag&lt;/code&gt; for Invalidation
&lt;/h3&gt;

&lt;p&gt;Instead of timed revalidation only, tag your cached data:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cacheTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getOrgProjects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;cacheTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`org-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-projects`&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then invalidate explicitly when something changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;revalidateTag&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProjectInput&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;project&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="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="nx"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nf"&gt;revalidateTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`org-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-projects`&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;project&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For SaaS apps with real mutations, this is far more useful than TTL-based revalidation. You get cached reads without stale data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Re-Measure
&lt;/h3&gt;

&lt;p&gt;Run the same metrics from Step 1. You should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard load times back to or better than baseline&lt;/li&gt;
&lt;li&gt;Database queries per minute lower than baseline (because your caching is now explicit and probably covers cases the old implicit cache missed)&lt;/li&gt;
&lt;li&gt;No stale data bugs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you don't, you missed something. The most common miss is a marketing or docs page under &lt;code&gt;app/&lt;/code&gt; that is now being rendered per-request for thousands of visitors.&lt;/p&gt;


&lt;p&gt;Explicit caching is more work upfront and much less work later. Implicit caching is the opposite — cheap to start with, expensive when something goes wrong and nobody can figure out why a value is stale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Specific Bugs I've Seen Post-Upgrade
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bug 1: "The Dashboard Got Slower"
&lt;/h3&gt;

&lt;p&gt;Usually this is a widget that was being served from the cached &lt;code&gt;fetch&lt;/code&gt; response and is now hitting the API on every page load. The fix is almost always: identify the three or four widgets that don't need to be live, and cache them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 2: "Some Users See Other Users' Data" (Rare but Serious)
&lt;/h3&gt;

&lt;p&gt;This is almost always a caching directive that was copied from a tutorial and applied to a user-scoped data function without &lt;code&gt;cacheTag&lt;/code&gt; including the user or org ID. Every cached function that returns per-user data must include the user or org in its cache key, full stop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug 3: "Revalidation Just Stopped Working"
&lt;/h3&gt;

&lt;p&gt;Usually someone kept their old &lt;code&gt;revalidate&lt;/code&gt; segment config around and added &lt;code&gt;'use cache'&lt;/code&gt; on top. The two don't compose the way you'd expect. Pick one strategy per route and stick with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Not Upgrade Yet
&lt;/h2&gt;

&lt;p&gt;Reasons to delay the Next.js 16 upgrade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're mid-launch and any instability is expensive&lt;/li&gt;
&lt;li&gt;You have complex custom caching logic that will need to be rewritten&lt;/li&gt;
&lt;li&gt;Your team is small and nobody has time to do the caching audit properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"We'll upgrade and fix caching later" is a trap. Upgrade when you can do the caching audit as part of the same work.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;The shift from implicit to explicit caching is part of a broader pattern in the App Router: fewer decisions are being made for you, and the ones that are get more visible.&lt;/p&gt;

&lt;p&gt;This is good for serious SaaS products. Caching is one of those areas where "it just works" is usually "it just works until it doesn't, and then nobody can figure out why." Explicit caching is slightly more verbose and dramatically more debuggable.&lt;/p&gt;

&lt;p&gt;It does mean the upgrade is not a drop-in replacement for most SaaS apps. Budget real time — not for the upgrade itself, which is fast, but for the caching audit that should go with it.&lt;/p&gt;


&lt;p&gt;If your SaaS product feels slower or heavier after the Next.js 16 upgrade, the fix is almost never in the framework. It is in the assumptions your code was making about caching that no longer hold.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  Does Next.js 16 break my existing app?
&lt;/h3&gt;

&lt;p&gt;Not directly. Your code still runs. What changes is behavior — specifically, data that used to be cached implicitly is now fetched fresh every request. The app works, but your database and API calls go up. Plan the upgrade alongside a caching audit, not as a standalone version bump.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to rewrite every &lt;code&gt;fetch()&lt;/code&gt; call in Next.js 16?
&lt;/h3&gt;

&lt;p&gt;Only the ones you actually want cached. In 16, &lt;code&gt;fetch()&lt;/code&gt; is fresh by default. If you had public, stable data (product catalogs, marketing content, public config) that was implicitly cached before, you'll need to add &lt;code&gt;cache: 'force-cache'&lt;/code&gt; and a &lt;code&gt;revalidate&lt;/code&gt; window to restore that behavior. Per-user and per-request data can stay as-is.&lt;/p&gt;

&lt;h3&gt;
  
  
  What replaces &lt;code&gt;unstable_cache&lt;/code&gt; in Next.js 16?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;'use cache'&lt;/code&gt; plus the &lt;code&gt;cacheLife&lt;/code&gt; and &lt;code&gt;cacheTag&lt;/code&gt; APIs. &lt;code&gt;unstable_cache&lt;/code&gt; still works for now but the ergonomics are worse and it's deprecated in the docs. If you're already in a Next.js 16 codebase doing caching work, migrate to &lt;code&gt;'use cache'&lt;/code&gt; in the same PR — it's a cleaner API and removes a deprecation you'd otherwise revisit in 17.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is &lt;code&gt;'use cache'&lt;/code&gt; stable in production?
&lt;/h3&gt;

&lt;p&gt;Yes, as of Next.js 16. It's no longer behind an experimental flag and it's the primary caching primitive going forward. The thing to be careful about is cache keys — any function with &lt;code&gt;'use cache'&lt;/code&gt; that returns per-user data must include the user or org ID via &lt;code&gt;cacheTag&lt;/code&gt;, or you'll cross-contaminate users.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Next.js 16's caching changes are an example of a framework asking you to be explicit about something that used to be implicit. For SaaS apps specifically, that's net positive — but only if you treat the upgrade as an opportunity to audit caching, not just bump a version number.&lt;/p&gt;

&lt;p&gt;If you're also dealing with broader performance issues that the caching changes surfaced — slow dashboards, heavy re-renders, or fragile data flows — &lt;a href="https://dev.to/blog/why-your-nextjs-app-feels-slow-after-launch"&gt;Why Your Next.js App Feels Slow After Launch&lt;/a&gt; covers the full picture, and &lt;a href="https://dev.to/blog/react-compiler-nextjs-16-production"&gt;React Compiler in Production&lt;/a&gt; is worth reading alongside this one.&lt;/p&gt;

&lt;p&gt;If your product is on Next.js and performance is actively hurting user experience, &lt;a href="https://dev.to/services/nextjs-performance-optimization"&gt;see Next.js Performance Optimization&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want someone to review your upgrade plan before you ship it, &lt;a href="https://dev.to/book"&gt;book a 20-minute strategy call&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>webperf</category>
      <category>ai</category>
    </item>
    <item>
      <title>How much should an MVP actually cost?</title>
      <dc:creator>Somnath Khadanga</dc:creator>
      <pubDate>Thu, 09 Apr 2026 16:47:11 +0000</pubDate>
      <link>https://dev.to/somnath_khadanga_2e5c2364/how-much-should-an-mvp-actually-cost-4h0k</link>
      <guid>https://dev.to/somnath_khadanga_2e5c2364/how-much-should-an-mvp-actually-cost-4h0k</guid>
      <description>&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%2Fok8lmurifqqhjigwovnt.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%2Fok8lmurifqqhjigwovnt.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
If you are planning a SaaS product, one of the first questions you will ask is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much should an MVP actually cost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The honest answer is that there is no single fixed price. The cost of a SaaS MVP depends much more on scope, product complexity, and launch quality than on the label "MVP" itself.&lt;/p&gt;

&lt;p&gt;A lot of founders make the mistake of thinking MVP means "cheap version."&lt;/p&gt;

&lt;p&gt;It usually should mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The smallest version of the product that solves a real problem and is credible enough to launch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is very different from a throwaway demo.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll break down what really affects &lt;strong&gt;SaaS MVP cost&lt;/strong&gt; in 2026, where founders overspend, where they cut the wrong corners, and how to think about budget in a practical way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Short Answer
&lt;/h2&gt;

&lt;p&gt;A SaaS MVP can cost very little if it is extremely narrow, but it can become expensive quickly when you add custom dashboards, billing, admin tools, advanced user roles, integrations, AI features, or production-quality requirements.&lt;/p&gt;

&lt;p&gt;So instead of asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What is the average SaaS MVP cost?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What is the smallest version of this product that is still useful, trustworthy, and launchable?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the question that usually leads to a smarter budget.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Drives SaaS MVP Cost
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Scope Is the Biggest Cost Factor
&lt;/h3&gt;

&lt;p&gt;Most MVP budgets do not break because of technology. They break because the scope is too loose.&lt;/p&gt;

&lt;p&gt;If your MVP includes only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User signup and login&lt;/li&gt;
&lt;li&gt;One core workflow&lt;/li&gt;
&lt;li&gt;One user dashboard&lt;/li&gt;
&lt;li&gt;Basic admin management&lt;/li&gt;
&lt;li&gt;Clean launch-ready UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a very different project from an MVP that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple user roles&lt;/li&gt;
&lt;li&gt;Subscriptions and billing&lt;/li&gt;
&lt;li&gt;AI workflows&lt;/li&gt;
&lt;li&gt;Document uploads&lt;/li&gt;
&lt;li&gt;Internal admin tools&lt;/li&gt;
&lt;li&gt;Notifications&lt;/li&gt;
&lt;li&gt;Analytics&lt;/li&gt;
&lt;li&gt;Third-party integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest pricing jump usually comes from trying to include version-two ideas inside version one.&lt;/p&gt;

&lt;p&gt;A founder often says they want an MVP, but the actual feature list looks closer to an early full product.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Product Complexity Matters More Than Page Count
&lt;/h3&gt;

&lt;p&gt;Many founders try to estimate cost based on how many screens the app has.&lt;/p&gt;

&lt;p&gt;That is usually the wrong way to think about it.&lt;/p&gt;

&lt;p&gt;A "simple" SaaS product with 8 screens can still be expensive if it has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex business logic&lt;/li&gt;
&lt;li&gt;Role-based access&lt;/li&gt;
&lt;li&gt;Multi-step workflows&lt;/li&gt;
&lt;li&gt;Dynamic dashboards&lt;/li&gt;
&lt;li&gt;Custom API integrations&lt;/li&gt;
&lt;li&gt;AI processing&lt;/li&gt;
&lt;li&gt;Billing rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meanwhile, a product with more screens can still be manageable if the workflows are straightforward.&lt;/p&gt;

&lt;p&gt;The important thing is not how many pages exist. It is how much logic, state, data flow, and backend behavior each page requires.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Design Quality Changes the Budget
&lt;/h3&gt;

&lt;p&gt;Some founders want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic, clean, usable UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Others want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Polished visual identity&lt;/li&gt;
&lt;li&gt;Custom UX details&lt;/li&gt;
&lt;li&gt;Motion&lt;/li&gt;
&lt;li&gt;Premium interactions&lt;/li&gt;
&lt;li&gt;Conversion-focused flows&lt;/li&gt;
&lt;li&gt;Responsive behavior tuned carefully across devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are valid.&lt;/p&gt;

&lt;p&gt;But they are not the same budget.&lt;/p&gt;

&lt;p&gt;If the goal is to validate quickly, a clean and credible UI is often enough. If the goal is also to impress early customers, investors, or partners, design quality becomes a bigger part of the MVP cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Auth, Roles, and Permissions Add More Work Than People Expect
&lt;/h3&gt;

&lt;p&gt;A lot of SaaS founders underestimate how much complexity comes from user management.&lt;/p&gt;

&lt;p&gt;The moment your product needs things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin vs user roles&lt;/li&gt;
&lt;li&gt;Workspace or team structure&lt;/li&gt;
&lt;li&gt;Invitations&lt;/li&gt;
&lt;li&gt;Permissions&lt;/li&gt;
&lt;li&gt;Approval flows&lt;/li&gt;
&lt;li&gt;Session handling&lt;/li&gt;
&lt;li&gt;Access control by feature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The MVP becomes more complex.&lt;/p&gt;

&lt;p&gt;This is one of the most common areas where "simple SaaS app" estimates stop being simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Payments and Billing Can Expand the Scope Fast
&lt;/h3&gt;

&lt;p&gt;If your MVP includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subscriptions&lt;/li&gt;
&lt;li&gt;Plan limits&lt;/li&gt;
&lt;li&gt;Trial logic&lt;/li&gt;
&lt;li&gt;Invoices&lt;/li&gt;
&lt;li&gt;Taxes&lt;/li&gt;
&lt;li&gt;Failed payment handling&lt;/li&gt;
&lt;li&gt;Coupon flows&lt;/li&gt;
&lt;li&gt;Team billing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the product is no longer just about your core feature. It also becomes a billing product.&lt;/p&gt;

&lt;p&gt;Payments are worth adding early if they are part of the business model, but they should be planned carefully because they add both engineering and product complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. AI Features Change Both Cost and Risk
&lt;/h3&gt;

&lt;p&gt;In 2026, many founders want AI inside the MVP from day one.&lt;/p&gt;

&lt;p&gt;Sometimes that makes sense. Sometimes it is a distraction.&lt;/p&gt;

&lt;p&gt;AI can increase MVP development cost through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prompt and workflow design&lt;/li&gt;
&lt;li&gt;Chat or copilot UX&lt;/li&gt;
&lt;li&gt;Retrieval and search systems&lt;/li&gt;
&lt;li&gt;Document processing&lt;/li&gt;
&lt;li&gt;Quality and fallback logic&lt;/li&gt;
&lt;li&gt;API usage costs&lt;/li&gt;
&lt;li&gt;Latency and reliability concerns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can we add AI?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Does AI make the first version meaningfully more useful?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is yes, it may belong in the MVP. If not, it may be smarter as a post-launch improvement.&lt;/p&gt;

&lt;p&gt;If AI is central to the first release, &lt;a href="https://dev.to/services/ai-saas-development"&gt;AI SaaS Development&lt;/a&gt; becomes part of the MVP scope, not just a future experiment.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Launch Quality Changes the Budget More Than Founders Think
&lt;/h3&gt;

&lt;p&gt;Two products can have the same features but very different cost depending on how seriously you take launch quality.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;h4&gt;
  
  
  Lower-Quality Launch
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Rushed structure&lt;/li&gt;
&lt;li&gt;Minimal testing&lt;/li&gt;
&lt;li&gt;Weak performance&lt;/li&gt;
&lt;li&gt;Weak error handling&lt;/li&gt;
&lt;li&gt;Messy deployment&lt;/li&gt;
&lt;li&gt;Fragile code decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Stronger Launch
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Cleaner architecture&lt;/li&gt;
&lt;li&gt;Better performance&lt;/li&gt;
&lt;li&gt;More reliable user flows&lt;/li&gt;
&lt;li&gt;Safer auth handling&lt;/li&gt;
&lt;li&gt;Stronger deployment setup&lt;/li&gt;
&lt;li&gt;More maintainable codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second one costs more initially.&lt;/p&gt;

&lt;p&gt;But it often costs less overall because you avoid expensive cleanup later.&lt;/p&gt;

&lt;p&gt;This is why I usually recommend building an MVP that is small in scope, not careless in quality.&lt;/p&gt;

&lt;p&gt;If the launch foundation already feels shaky, &lt;a href="https://somanathkhadanga.com/services/production-readiness-upgrade" rel="noopener noreferrer"&gt;Production Readiness Upgrade&lt;/a&gt; is the kind of follow-up work that prevents an early MVP from turning into a cleanup project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Founders Usually Overspend
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overspending Pattern 1: Trying to Impress Everyone in Version One
&lt;/h3&gt;

&lt;p&gt;Many MVPs become bloated because the founder wants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Investor-ready polish&lt;/li&gt;
&lt;li&gt;Customer-ready features&lt;/li&gt;
&lt;li&gt;Admin-ready controls&lt;/li&gt;
&lt;li&gt;Marketing-ready analytics&lt;/li&gt;
&lt;li&gt;Enterprise-ready permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All at once.&lt;/p&gt;

&lt;p&gt;That turns an MVP into a much bigger product before market validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overspending Pattern 2: Unclear Priorities
&lt;/h3&gt;

&lt;p&gt;If every feature feels important, the budget rises fast.&lt;/p&gt;

&lt;p&gt;A better approach is to split features into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Must-have for first launch&lt;/li&gt;
&lt;li&gt;Useful soon after launch&lt;/li&gt;
&lt;li&gt;Wait until users prove demand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This usually reduces both cost and time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Overspending Pattern 3: Adding Too Many Integrations Early
&lt;/h3&gt;

&lt;p&gt;Integrations often sound small in planning and become large in implementation.&lt;/p&gt;

&lt;p&gt;Every integration adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup&lt;/li&gt;
&lt;li&gt;Edge cases&lt;/li&gt;
&lt;li&gt;Maintenance&lt;/li&gt;
&lt;li&gt;Sync logic&lt;/li&gt;
&lt;li&gt;Failure handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it is not central to the MVP, it is often better to delay it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Founders Cut the Wrong Corners
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Wrong Shortcut 1: Treating the MVP Like a Throwaway Build
&lt;/h3&gt;

&lt;p&gt;If the product works, gets users, and shows traction, you will want to keep building on it.&lt;/p&gt;

&lt;p&gt;A bad foundation can become more expensive than starting properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong Shortcut 2: Ignoring Performance Until Later
&lt;/h3&gt;

&lt;p&gt;Many SaaS products feel slow right after launch because performance was not considered during the MVP phase.&lt;/p&gt;

&lt;p&gt;That slows onboarding, hurts trust, and makes the product feel weaker than it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong Shortcut 3: Weak Auth and Admin Flows
&lt;/h3&gt;

&lt;p&gt;A lot of early products underestimate how important clean auth, role handling, and admin visibility are.&lt;/p&gt;

&lt;p&gt;These are often the parts that make a product feel real rather than half-finished.&lt;/p&gt;

&lt;p&gt;If you want to avoid those problems, this is exactly the kind of thinking I bring to &lt;a href="https://somanathkhadanga.com/services/saas-mvp-development" rel="noopener noreferrer"&gt;SaaS MVP development&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Better Way to Think About MVP Budget
&lt;/h2&gt;

&lt;p&gt;Instead of asking for one big estimate for "the whole app," break it into these layers:&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Core User Problem
&lt;/h3&gt;

&lt;p&gt;What is the single most important workflow?&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Minimum Product Credibility
&lt;/h3&gt;

&lt;p&gt;What does the product need so real users will trust it enough to try it?&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Launch Essentials
&lt;/h3&gt;

&lt;p&gt;What needs to exist for a real release, not just an internal demo?&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: Delay List
&lt;/h3&gt;

&lt;p&gt;What can wait until after the first feedback cycle?&lt;/p&gt;

&lt;p&gt;This framework usually leads to a much more realistic MVP budget.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Practical Advice for Founders
&lt;/h2&gt;

&lt;p&gt;If you want a smarter MVP budget in 2026, do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with one painful problem&lt;/li&gt;
&lt;li&gt;Keep the feature set narrow&lt;/li&gt;
&lt;li&gt;Be serious about fundamentals&lt;/li&gt;
&lt;li&gt;Separate MVP from roadmap&lt;/li&gt;
&lt;li&gt;Budget for launch, not just coding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A product that only "exists" is not the same as a product that is ready to launch.&lt;/p&gt;

&lt;p&gt;If you want more context on that distinction, read &lt;a href="https://somanathkhadanga.com/blog/production-ready-saas-mvp" rel="noopener noreferrer"&gt;What Makes a SaaS MVP Production-Ready (Most MVPs Are Not)&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  So How Much Should a SaaS MVP Cost?
&lt;/h2&gt;

&lt;p&gt;The real answer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It should cost enough to launch something useful and credible, but not so much that you build version two before validating version one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the balance.&lt;/p&gt;

&lt;p&gt;A strong MVP is not the cheapest version. It is the smallest version worth shipping.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Talk to a Developer Early
&lt;/h2&gt;

&lt;p&gt;You should talk to a technical partner early if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your feature list keeps growing&lt;/li&gt;
&lt;li&gt;You are unsure what belongs in version one&lt;/li&gt;
&lt;li&gt;AI is part of the product&lt;/li&gt;
&lt;li&gt;You need user roles, billing, dashboards, or admin flows&lt;/li&gt;
&lt;li&gt;You want to avoid rebuilding after launch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That early clarity can save a surprising amount of time and budget.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: SaaS MVP Cost in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How much does a SaaS MVP cost in 2026?
&lt;/h3&gt;

&lt;p&gt;It depends on the feature set, product complexity, launch quality, and whether the MVP includes things like billing, AI, admin tools, and multiple user roles. The real cost question is whether the scope is disciplined enough for a useful first release.&lt;/p&gt;

&lt;h3&gt;
  
  
  What increases MVP development cost the fastest?
&lt;/h3&gt;

&lt;p&gt;Loose scope is usually the biggest reason cost rises. After that, the biggest multipliers are billing, permissions, integrations, AI workflows, and trying to include roadmap features in version one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should AI be in a SaaS MVP from day one?
&lt;/h3&gt;

&lt;p&gt;Only if it makes the first version materially more useful. If AI is interesting but not essential, it is often better added after launch once the core workflow is validated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Most founders do not actually need the biggest possible MVP.&lt;/p&gt;

&lt;p&gt;They need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The right scope&lt;/li&gt;
&lt;li&gt;The right technical decisions&lt;/li&gt;
&lt;li&gt;The right launch quality&lt;/li&gt;
&lt;li&gt;The right sequencing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is what keeps cost under control without creating technical debt on day one.&lt;/p&gt;

&lt;p&gt;If you are planning an MVP and want to launch without building version one the wrong way, &lt;a href="https://somanathkhadanga.com/services/saas-mvp-development" rel="noopener noreferrer"&gt;see SaaS MVP Development&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>development</category>
      <category>mvp</category>
    </item>
    <item>
      <title>Tired of ChatGPT "forgetting" context, so I engineered a Private "Second Brain" using MERN &amp; Local Llama 3 🧠</title>
      <dc:creator>Somnath Khadanga</dc:creator>
      <pubDate>Fri, 23 Jan 2026 15:56:20 +0000</pubDate>
      <link>https://dev.to/somnath_khadanga_2e5c2364/tired-of-chatgpt-forgetting-context-so-i-engineered-a-private-second-brain-using-mern-local-5al3</link>
      <guid>https://dev.to/somnath_khadanga_2e5c2364/tired-of-chatgpt-forgetting-context-so-i-engineered-a-private-second-brain-using-mern-local-5al3</guid>
      <description>&lt;p&gt;As a full-stack developer juggling multiple projects, context switching is my biggest productivity killer. I use AI tools daily, but they have two major flaws for professional workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They Forget:&lt;/strong&gt; Start a new chat, and the context is gone. They don't remember the bug I debugged yesterday or the specific architectural constraints of my current project.&lt;/p&gt;

&lt;p&gt;Privacy Anxiety: There are times I want to paste sensitive client logic or proprietary snippets, but sending that data to a cloud API feels risky.&lt;/p&gt;

&lt;p&gt;I realized I didn't just need a chatbot; I needed a persistent, private "Second Brain" that lived on my machine and knew my work history.&lt;/p&gt;

&lt;p&gt;Instead of waiting for a product to solve this, I decided to engineer my own solution using the stack I know best.&lt;/p&gt;

&lt;p&gt;The High-Level Architecture&lt;br&gt;
The goal was to build a system that runs 100% locally—no internet connection required for inference, no data leaving my laptop.&lt;/p&gt;

&lt;p&gt;Here is the system design I came up with:&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%2Fyzuptd2z1z2x0mp3f3bt.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%2Fyzuptd2z1z2x0mp3f3bt.png" alt=" " width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Here is why I chose these specific tools for the job:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend: React (Vite)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why: I needed a snappy, familiar chat interface. React’s component-based architecture makes it easy to manage chat history state and streaming responses.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Backend: Node.js / Express&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Why: It’s the glue. Node acts as the orchestration layer, handling API requests from the frontend, managing file uploads for memory, and communicating asynchronously with the AI engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Brain: Ollama running Llama 3 (8B)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why: Ollama is hands-down the easiest way to run local models. I chose Llama 3 8B because it hits the sweet spot for my hardware—it's fast enough for real-time chat but smart enough to follow complex instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Memory (RAG): ChromaDB (running locally)
&lt;/h2&gt;

&lt;p&gt;Why: This is the core of the "Second Brain." I needed a Vector Database to store embeddings of my notes and code. I chose ChromaDB because it's open-source, easy to run locally via Docker, and integrates well with JavaScript ecosystems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenges: It's Not Magic
&lt;/h2&gt;

&lt;p&gt;Any senior developer knows that the "happy path" is only 20% of the work. The biggest challenge wasn't getting the components to talk to each other; it was Retrieval Accuracy.&lt;/p&gt;

&lt;p&gt;Initially, the RAG pipeline was "dumb." It would fetch documents based on simple keyword matching, confusing the LLM with irrelevant context.&lt;/p&gt;

&lt;p&gt;The fix (currently in progress): I'm experimenting with smaller, more semantic chunk sizes and looking into implementing a "re-ranking" step—where we retrieve 20 documents but have a smaller, faster model sort them by relevance before sending top 5 to Llama 3. This significantly improves the quality of answers.&lt;/p&gt;

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

&lt;p&gt;This project is still very much a work in progress. It’s messy, but it’s mine, and most importantly, it’s private.&lt;/p&gt;

&lt;p&gt;It has forced me to dive deep into the mechanics of Vector Databases and local inference, skills that are becoming essential for modern backend engineering.&lt;/p&gt;

&lt;p&gt;If you’re interested in seeing the final polished version or following my journey as I build this out in public:&lt;/p&gt;

&lt;p&gt;Follow me on Twitter/X: [👉 &lt;a href="https://x.com/khadangasomnath" rel="noopener noreferrer"&gt;https://www.somanathkhadanga.com/&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Check out my other projects: [👉 &lt;a href="https://www.somanathkhadanga.com/" rel="noopener noreferrer"&gt;https://www.somanathkhadanga.com/&lt;/a&gt;]&lt;/p&gt;

</description>
      <category>rag</category>
      <category>mern</category>
      <category>ai</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
