DEV Community

Cover image for AI Generated 47% of My Code Last Month. Here's What I Actually Had to Fix (And Why I Didn't Trust It With Auth)
Shola Jegede
Shola Jegede Subscriber

Posted on

AI Generated 47% of My Code Last Month. Here's What I Actually Had to Fix (And Why I Didn't Trust It With Auth)

It was 3:47 AM when my phone lit up. "Error: Maximum call stack size exceeded." My SaaS app was down. Users were waking up to broken dashboards, and I was staring at a stack trace pointing to code I barely recognized.

Two weeks earlier, I'd been bragging about shipping features 3x faster with v0.app and GitHub Copilot. I'd generated entire components with a single prompt, scaffolded API routes in seconds, built what looked like a production-ready application in a fraction of the usual time. Now, at nearly 4 AM, I was debugging code I hadn't actually written.

The authentication middleware was calling itself recursively under specific edge cases. The AI had generated elegant-looking code that handled 99% of scenarios perfectly. It just created an infinite loop for that remaining 1%.

When I finally resolved the issue and analyzed my codebase, the numbers shocked me: 47% of my code was AI-generated. Not suggested and then heavily modified—actually written by AI tools with minimal changes from me.

This isn't just my story. 41% of all code is now AI-generated, with 76% of developers using or planning to use AI coding tools. But nobody talks about what happens after the code is generated, especially when it comes to security.

Over the past month, I catalogued every bug, every fix, every security hole. What I found falls into five distinct categories, and one critical decision: I chose Kinde for authentication because I refused to let AI touch my security layer.

The Tools and The Numbers

I used v0.app for frontend work and GitHub Copilot for everything else. Both are genuinely impressive. v0's composite pipeline catches errors in real-time. Copilot's autocomplete is exceptionally good at common patterns.

My final codebase: 8,500 lines of code. 4,000 lines came primarily from AI tools. But the distribution reveals what AI is actually good at:

  • Frontend Components: 65% AI-generated (v0.app)
  • API Routes: 40% AI-generated (Copilot)
  • Database Schema: 30% AI-generated (Copilot)
  • Auth Integration: 20% AI-generated (I wrote the rest myself with Kinde)
  • Utility Functions: 55% AI-generated (Copilot)

AI excelled at presentational code and struggled with business logic. The closer code got to security, data integrity, or domain-specific rules, the less I trusted AI—and the more dangerous it was to let it write anything unsupervised.

The 5 Categories of Bugs

1. Hallucinations: Code That References Things That Don't Exist

Early in development, Copilot suggested this for user registration:

import { validateEmail, sanitizeInput } from '@utils/email-validator';
import { checkPasswordStrength} from '@security/password-utils';

export function validateRegistrationForm(data: {
  email: string;
  password: string;
  name: string;
}) {
  if (!validateEmail(data.email)) {
    throw new Error('Invalid email address');
  }

  const sanitized = sanitizeInput(data.name);
  const passwordCheck = checkPasswordStrength(data.password);

  if (passwordCheck.score < 3) {
    throw new Error('Password too weak');
  }

  return { email: data.email, name: sanitized, password: data.password };
}
Enter fullscreen mode Exit fullscreen mode

Professional variable names. Clear logic. Proper error handling. One problem: none of those imported functions existed. Not in my project, not in npm, not anywhere.

The research is alarming: a study of 576,000 code samples found 440,445 hallucinated packages. Nearly half a million references to things that don't exist. Worse, malicious actors can create packages with commonly hallucinated names containing malware.

Copilot also suggested Convex had an update method (it's patch) and a db.notifyUser method (complete fiction).

How to spot hallucinations:

  • IDE warnings are your friend. Investigate every red squiggly line.
  • If it sounds too convenient, verify it exists.
  • Before npm install, check the package registry.
  • Framework-specific methods need double-checking against docs.
  • Use TypeScript strict mode.

2. Context Blindness: Ignoring Your Architecture

My app used Convex for real-time data synchronization. When I asked v0 to create a task list component, it gave me classic REST-style React with useState, useEffect, manual fetching, and polling for updates every 5 seconds.

But we're using Convex. Here's what it should have been:

"use client";

import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";

export function TaskList() {
  const { user } = useKindeBrowserClient();
  const tasks = useQuery(
    api.tasks.getUserTasks,
    user ? { userId: user.id } : "skip"
  );

  if (tasks === undefined) return <div>Loading...</div>;
  if (tasks.length === 0) return <div>No tasks yet.</div>;

  return (
    <div className="space-y-4">
      {tasks.map((task) => (
        <TaskCard key={task._id} task={task} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

No useState, no useEffect, no manual fetching, no polling. Convex's useQuery handles all of that. When data changes, the component automatically re-renders. Simpler, more reliable, less code.

Copilot made similar mistakes with database queries, generating N+1 query problems. With 50 tasks, that's 51 database queries instead of 2.

How to prevent context blindness:

  • Provide explicit context in prompts
  • Create a CONVENTIONS.md file documenting your patterns
  • Use linters to enforce patterns
  • Build reusable abstractions that enforce conventions

3. "Almost Right": Subtle Logic Flaws

66% of developers say the biggest issue with AI tools is solutions that are almost right, but not quite. The code runs without errors. TypeScript is happy. But there's a logic flaw that only reveals itself under specific conditions.

When I asked Copilot for pagination, it generated code that calculated offsets wrong—page 1 showed items 10-19, not 0-9. It also loaded ALL tasks into memory before slicing, meaning with 10,000 tasks, we're loading all 10,000 to return 10.

Another example: Copilot generated date arithmetic for recurring tasks that worked perfectly for December 1st + 1 month, but January 31st + 1 month = March 3rd because JavaScript's setMonth() handles overflow by rolling into the next month.

The scariest bug was silent data loss. Copilot generated code that replaced an entire preferences object instead of merging it. The mutation succeeded, user got a success message, everything seemed fine—until they opened the app and wondered why their language and timezone settings were gone.

How to catch these:

  • Test edge cases explicitly (end-of-month, leap years, zero values, empty arrays)
  • Use property-based testing
  • Code review with skepticism
  • Add assertions liberally

4. Security Vulnerabilities: Why I Chose Kinde

This is where I drew the line. 48% of AI-generated code contains security vulnerabilities. After finding my first missing auth check, I made a decision: authentication would be the ONE thing I wouldn't let AI touch.

Here's what Copilot generated for a task deletion endpoint:

// Copilot's security hole
export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    // No authentication check!
    await convex.mutation(api.tasks.deleteTask, {
      taskId: params.id,
    });
    return NextResponse.json({ success: true });
  } catch (error) {
    return NextResponse.json({ error: "Failed" }, { status: 500 });
  }
}
Enter fullscreen mode Exit fullscreen mode

This code works. It deletes tasks. Anyone can call this endpoint and delete any task. No authentication required.

I initially missed two similar endpoints with the same vulnerability. They were exposed for three days before I found them during a security audit. That's when I decided: no more AI-generated auth code.

Why I Chose Kinde (Not AI) for Authentication

After finding security holes in AI-generated auth code, I made a critical decision: authentication would be handled by Kinde, a proven platform that I could trust—not by AI guessing at security patterns.

The moment that sealed it? I asked Copilot to generate middleware for route protection. It gave me code that looked professional but had three subtle vulnerabilities: it cached authentication state incorrectly, didn't handle token refresh, and had a race condition during session validation. These aren't bugs you catch in testing. They're production nightmares waiting to happen.

I chose Kinde because:

1. Pre-built security that AI can't hallucinate

Authentication, authorization, session management, and MFA all handled by Kinde. No AI making up phantom security methods. No missing auth checks. No subtle vulnerabilities that only surface under load.

Kinde provides everything you need out of the box:

  • OAuth integration (Google, GitHub, Microsoft, etc.)
  • Multi-factor authentication
  • Session management with automatic token refresh
  • Role-based access control
  • Organization management for B2B apps
  • Webhooks for user events

Most importantly, Kinde's SDK methods actually exist and do what they claim. When AI suggested getKindeServerSession().validateToken() (which doesn't exist), I could verify against Kinde's docs that the correct pattern is isAuthenticated().

2. Verifiable documentation I could trust

When AI generated incorrect Kinde code, I could verify against real docs. Every method exists. Every pattern works. Here's the correct pattern Kinde documents:

// app/api/tasks/[id]/route.ts - Verified Kinde pattern
import { NextRequest, NextResponse } from "next/server";
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
import { ConvexHttpClient } from "convex/browser";
import { api } from "@/convex/_generated/api";

const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  // CRITICAL: Kinde handles auth correctly
  const { getUser, isAuthenticated } = getKindeServerSession();

  if (!(await isAuthenticated())) {
    return NextResponse.json(
      { error: "Authentication required" },
      { status: 401 }
    );
  }

  const user = await getUser();

  try {
    // Pass user ID to Convex for ownership verification
    await convex.mutation(api.tasks.deleteTask, {
      taskId: params.id,
      userId: user.id,
    });

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error("Delete error:", error);
    return NextResponse.json(
      { error: "Failed to delete task" },
      { status: 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

And the corresponding Convex mutation with authorization:

// convex/tasks.ts - Defense in depth with Convex
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const deleteTask = mutation({
  args: {
    taskId: v.id("tasks"),
    userId: v.string(),
  },
  handler: async (ctx, args) => {
    // Verify auth at Convex level too
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw new Error("Not authenticated");
    }

    // Verify user ID matches
    if (identity.subject !== args.userId) {
      throw new Error("User ID mismatch");
    }

    // Get task to verify ownership
    const task = await ctx.db.get(args.taskId);
    if (!task) {
      throw new Error("Task not found");
    }

    // CRITICAL: Verify ownership
    if (task.userId !== args.userId) {
      throw new Error("Not authorized to delete this task");
    }

    // Only now is it safe to delete
    await ctx.db.delete(args.taskId);
    return { success: true };
  },
});
Enter fullscreen mode Exit fullscreen mode

This implements defense in depth: authentication at the API route level, authorization in the Convex mutation, and ownership verification before deletion. All three layers are necessary—and all three would have been compromised if I'd let AI write them.

3. Seamless integration with my stack

Kinde works perfectly with Convex and Next.js. Setting up the integration took 15 minutes:

// app/api/auth/[kindeAuth]/route.ts - Single file setup
import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server";

export async function GET(request: Request, { params }: any) {
  const endpoint = params.kindeAuth;
  return handleAuth(request, endpoint);
}
Enter fullscreen mode Exit fullscreen mode

That's it. One file handles login, logout, callbacks, and token refresh. Compare that to the 200+ lines of auth code Copilot generated that I had to debug.

The client-side integration is equally clean:

// app/tasks/page.tsx - Server-side with Kinde
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";
import { redirect } from "next/navigation";

export default async function TasksPage() {
  const { isAuthenticated } = getKindeServerSession();

  if (!(await isAuthenticated())) {
    redirect("/api/auth/login");
  }

  return <TaskListClient />;
}
Enter fullscreen mode Exit fullscreen mode

For client components, Kinde provides a dedicated hook:

// components/UserProfile.tsx - Client-side
"use client";

import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";

export function UserProfile() {
  const { user, isLoading } = useKindeBrowserClient();

  if (isLoading) return <div>Loading...</div>;
  if (!user) return null;

  return (
    <div>
      <img src={user.picture} alt={user.given_name} />
      <p>{user.email}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

And the Convex integration pattern:

// convex/tasks.ts - Server-side auth helper
async function getAuthUser(ctx) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) throw new Error("Not authenticated");
  return identity;
}

export const getUserTasks = query({
  handler: async (ctx) => {
    const user = await getAuthUser(ctx);

    return await ctx.db
      .query("tasks")
      .withIndex("by_user", (q) => q.eq("userId", user.subject))
      .collect();
  },
});
Enter fullscreen mode Exit fullscreen mode

4. Organizations and permissions built-in

My app needed team collaboration features. Copilot generated a custom permission system with 300+ lines of code. It had bugs in role inheritance, missing permission checks in mutations, and no audit trail.

Kinde's organization features handled this in 10 lines:

export async function POST(request: Request) {
  const { getUser, getOrganization } = getKindeServerSession();

  const user = await getUser();
  const org = await getOrganization();

  // Kinde handles organization membership automatically
  if (!org) {
    return NextResponse.json(
      { error: "No organization" },
      { status: 403 }
    );
  }

  // Create task within organization context
  await convex.mutation(api.tasks.create, {
    ...body,
    organizationId: org.orgCode,
    userId: user.id,
  });
}
Enter fullscreen mode Exit fullscreen mode

Would AI have generated this correctly? Based on the 48% vulnerability rate and the hallucinations I found, probably not. Kinde gave me production-grade auth I could trust, with patterns I could verify against documentation instead of hoping the AI got it right.

The security incident that never happened:

Two weeks after launch, I ran a security audit. Every auth check was in place. Every permission was verified. Every session was handled correctly. The reason? I didn't trust AI to write security code, and Kinde ensured I didn't have to.

Other vulnerabilities I found in AI-generated code:

  • Missing input validation (no length checks, no sanitization)
  • Information disclosure (error handlers exposing stack traces)
  • IDOR vulnerabilities (trusting client-provided IDs without ownership checks)

Key takeaway: Security vulnerabilities in AI-generated code are systemic, not accidental. Kinde solved this by giving me battle-tested authentication that I didn't have to second-guess at 3:47 AM.

5. Performance Killers: Code That Works But Performs Terribly

AI models optimize for correctness, not efficiency. Copilot loves N+1 queries:

// Copilot's N+1 disaster  
export const getProjectsWithTasks = query({
  handler: async (ctx) => {
    const projects = await ctx.db
      .query("projects")
      .withIndex("by_user", (q) => q.eq("userId", identity.subject))
      .collect();

    // One query for EACH project's tasks
    const projectsWithTasks = await Promise.all(
      projects.map(async (project) => {
        const tasks = await ctx.db
          .query("tasks")
          .filter((q) => q.eq(q.field("projectId"), project._id))
          .collect();
        return { ...project, tasks };
      })
    );

    return projectsWithTasks;
  },
});
Enter fullscreen mode Exit fullscreen mode

With 10 projects, this runs 11 database queries. With 100 projects, 101 queries. During development with 3 test projects, it ran in 50ms. With a realistic dataset of 50 projects, it took 2.5 seconds.

The fix: batch everything into 2 queries and combine in memory using Convex's indexes and proper query patterns.

The Hidden Costs

Beyond bugs, there are costs that accumulate slowly:

Technical Debt: GitClear's 2025 report found duplicated code blocks increased eightfold. I found four different implementations of email validation scattered across my codebase, each slightly different.

Review Burden: Pre-AI, I reviewed 200 lines in 20 minutes. With AI code, that same 200 lines took 40-50 minutes. I had to verify everything was real, check for security issues, assess performance, and ensure consistency.

Learning Impediment: When AI writes the code, you skip the learning process. Three months later, when I needed to modify AI-generated presence tracking code, I realized I didn't understand how it worked. I had working code but no mental model.

What Actually Saved Time

Despite all this, AI saved me massive amounts of time. I shipped in 70 hours what would have taken 120 manually—a 40% time savings.

Where AI excelled:

  • Boilerplate and scaffolding (80% time savings)
  • Type definitions (90% time savings)
  • Test scaffolding (70% time savings)
  • API route ceremony (75% time savings)

Where human expertise was essential:

  • Architecture decisions (100% human)
  • Security implementation (90% human—Kinde handled the rest)
  • Business logic (85% human)
  • System integration (80% human)

The Real Math

The Promise: Generate code 3-5x faster.

The Reality: Generate 3-5x faster, but spend 2-3x longer reviewing and fixing. Net result: 30-40% faster overall.

That's still significant—but it's honest math accounting for review time, debugging, refactoring, and the decision to use Kinde instead of trusting AI with authentication.

Five Lessons That Matter Most

  1. AI is a tool for iteration, not generation. Use it for starting points, then iterate with human judgment.

  2. Treat every AI suggestion as a hypothesis. Verify everything—especially authentication, authorization, and business logic.

  3. Security cannot be delegated to AI. Use proven platforms like Kinde. Every security decision requires human review.

  4. Strong engineering practices are your safety net. TypeScript strict mode, linting, testing, and code review are essential with AI code.

  5. Use AI strategically, not universally. Let AI handle boilerplate. Keep architecture, business logic, and security in human hands.

The Path Forward

256 billion lines of AI-generated code have been written as of 2024. The models will improve. But fundamentally, AI pattern-matches against massive datasets. It doesn't understand your business requirements, architectural constraints, or security needs.

The developers who thrive won't be the ones generating the most code. They'll be the ones who know exactly what to delegate to AI and what requires human expertise—or proven platforms like Kinde.

AI generated 47% of my code last month. It helped me ship faster than I could have alone. But every bug I fixed, every security hole I patched with Kinde, every performance optimization I made—those taught me more about software engineering than any AI-generated code could have.

The future isn't "AI writes all the code." It's AI and humans collaborating, each doing what they do best. AI generates patterns. Humans provide judgment. AI creates volume. Humans ensure quality. And platforms like Kinde handle security so you don't have to trust AI with the parts that matter most.

Use AI. But use it wisely. Choose Kinde for auth. Review carefully. Test thoroughly. Ship confidently.

And maybe invest in a better notification sound for your production monitoring.

Ready to build with confidence? Check out Kinde's documentation for production-ready authentication that you can trust—no AI guessing required. And explore Convex's guides for type-safe, real-time databases that integrate seamlessly with Kinde's battle-tested security.

Top comments (0)