Back to blog
·6 min read

Why Claude Code Misses Security Vulnerabilities in Supabase: A Next.js Developer's Guide to AI Code Review

Claude Code generates functional code fast, but it often misses critical security holes—especially in Supabase Row Level Security, connection methods, and database queries. Here's how to catch what AI assistants miss.

You've just asked Claude Code to build a user authentication system for your Next.js app connected to Supabase. The code compiles. It works. Users can sign up, log in, and create profiles. You push to production on Friday.

By Monday, you realize there's a problem: your Supabase dashboard is showing queries from IP addresses you don't recognize. Someone discovered that your anonymous key (the one published in your Next.js environment variables) grants direct table access. Row Level Security wasn't enabled. They've been reading user data all weekend.

This scenario happens more often than you'd think, and it's not because Claude Code is bad. It's that AI assistants are optimized for functional correctness, not security architecture. They'll write code that works and passes your tests, while missing security holes that experienced developers catch in seconds.

Why Claude Code and AI Agents Miss Security Vulnerabilities

Claude Code operates within constraints that work against security:

  • It optimizes for features that work locally. Your development database has 40 rows, so N+1 query problems are invisible.
  • It switches between connection methods mid-project. You might end up with direct Postgres connections, Supabase JS SDK calls, and Drizzle ORM all in the same codebase—making it impossible to audit.
  • It skips security architecture because it's not prompted. Security feels abstract to an AI. You have to explicitly ask for it.
  • It doesn't think about observability. No logging, no error tracking, no audit trails. When something goes wrong in production, you're blind.
  • The biggest vulnerability in AI-generated code isn't a specific bug. It's that the code has no unified picture of what's actually happening at runtime.

    The Supabase Row Level Security Problem

    Here's the most common failure I see in AI-generated Supabase code:

    A developer asks Claude Code: "Build a user profiles table where users can only see their own data."

    Claude Code responds with a working schema and a Next.js API route. But there's no Row Level Security policy. The code works because it checks auth on the client side. But the Supabase table itself is wide open.

    Here's what that table should look like:

    ```sql

    create table profiles (

    id uuid primary key references auth.users(id),

    email text unique not null,

    full_name text,

    created_at timestamp default now()

    );

    alter table profiles enable row level security;

    create policy "Users can see only their own profile"

    on profiles for select

    using (auth.uid() = id);

    create policy "Users can update only their own profile"

    on profiles for update

    using (auth.uid() = id);

    ```

    Claude Code often skips the RLS policies. The code "works" in development because you're testing with your own user ID. In production, anyone with the anon key can query the profiles table directly.

    The fix: Always enable RLS on every Supabase table that contains user data. Make it your first reflex. Before you ask Claude Code to generate anything, create your table, enable RLS, and write the policies. Then ask Claude Code to build the application code that assumes RLS is already in place.

    The Multiple Connection Method Problem

    Another pattern I see: Claude Code will generate code using three different strategies to talk to Supabase, often in the same project:

  • One page uses the Supabase JS client directly
  • Another uses Supabase Server Actions with the admin client
  • A third uses Drizzle ORM with Supabase Postgres
  • Each connection method has different auth contexts, different error handling, different performance characteristics. When something breaks, you're debugging across three different paradigms.

    The solution: Pick one connection strategy and stick with it. For most Next.js + Supabase applications, this means:

  • Use Supabase Auth for user authentication
  • Use Supabase JavaScript client in the browser
  • Use the admin client (with service role key) only in Server Actions
  • Use Drizzle ORM if you need type safety across all queries
  • Write this architecture decision down. Share it with Claude Code in a CLAUDE.md file before you start building. This single document cuts debugging time in half because Claude Code will follow your established pattern.

    The N+1 Query Problem That AI Never Catches Locally

    Your dev database has 100 users. Your production database has 100,000. Claude Code generates code that fetches a user, then fetches their organization, then loops through 50 related records one at a time.

    Locally: 2ms. Production: 12 seconds.

    AI assistants can't see this problem until it's in production. You have to build it in. Set up your development environment to match production scale. Use Supabase's free tier and seed it with realistic data volumes. Run performance tests before you push code.

    When you ask Claude Code to generate database queries, include specific guidance: "Fetch user and organization in a single query using joins. The organization table has 50,000 rows."

    The Observability Failure

    Here's what AI-generated code rarely includes: logging, error tracking, audit trails, and performance monitoring.

    Your Next.js Server Action succeeds 99% of the time, but when it fails, there's no record of what went wrong. Claude Code will write:

    ```javascript

    const response = await supabase

    .from('profiles')

    .select('*')

    .eq('id', userId);

    ```

    It should be:

    ```javascript

    try {

    const { data, error } = await supabase

    .from('profiles')

    .select('*')

    .eq('id', userId);

    if (error) {

    console.error('Profile fetch failed:', { userId, error });

    throw new Error('Failed to load profile');

    }

    return data;

    } catch (err) {

    logError(err, { context: 'fetchUserProfile', userId });

    throw err;

    }

    ```

    You'll never catch this unless you specifically ask Claude Code to include it. Make error handling and logging a requirement, not a feature request.

    Building Secure Applications with AI Assistance

    Here's the practical workflow:

  • Design your data model and security policies before asking Claude Code to build anything. Write them down.
  • Enable RLS on every table. Make it the first line of your schema, not an afterthought.
  • Choose one connection method and document it. Share this with Claude Code in your project context.
  • Request observability upfront. Ask for error handling, structured logging, and monitoring hooks in every data mutation.
  • Test with realistic data volumes. Seed your development database with 10,000+ rows so you can catch N+1 queries before production.
  • Review generated database code like you're a security auditor. Look for RLS policy enforcement, connection method consistency, and query efficiency.
  • If you're building a SaaS product with Next.js and Supabase, these patterns take time to establish correctly. Platforms like ZipBuild scaffold these architectural decisions upfront, so you start with secure, auditable patterns instead of discovering security holes after launch.

    The best use of Claude Code isn't replacing engineering judgment. It's automating implementation once you've made the hard architectural decisions.

    Try the free discovery chat at zipbuild.dev to see how structured scaffolding can accelerate your Next.js + Supabase development without compromising security.

    Written by ZipBuild Team

    Ready to build with structure?

    Try the free discovery chat and see how ZipBuild architects your idea.

    Start Building