Back to blog
·6 min read

How to Set Up Row Level Security in Supabase + Next.js Without Breaking Your App

Most developers build Next.js apps with Supabase but never enable Row Level Security, leaving all their data exposed. This guide shows you exactly how to configure RLS properly, structure your project to avoid hydration errors, and use AI coding tools effectively without cutting corners.

You just finished your Supabase + Next.js authentication setup. Data flows perfectly in development. You're shipping it to production feeling good. Then you realize: your Row Level Security policies are completely empty, and anyone with your database URL can read everything.

This is the most common security mistake developers make with Supabase. You enable auth, you set up users, but RLS stays disabled or misconfigured. The problem is silent—your app works fine locally because you're authenticated, but in production you're one SQL injection away from a data breach.

Let's fix this properly, from security configuration to project structure to how you use AI tools to scaffold it all without missing critical details.

Understanding Why RLS Defaults Fail Developers

Supabase starts with everything locked down by default. No RLS policies means no access. Sounds secure, right? But most developers interpret "no access" as "I'll set it up later" and never actually do it.

Here's what happens: you query a table without RLS policies enabled, get an empty array back, assume it's a bug, turn off RLS to fix it, and suddenly your data is publicly readable. You think you'll implement security later. You don't.

The confusion comes from conflating two different things:

  • RLS enable/disable toggle (whether policies are enforced at all)
  • RLS policies (the actual rules that determine who can see what)
  • You need both. The toggle must be ON, and you must write policies. No shortcuts.

    The Right Way to Enable and Configure RLS

    First, enable RLS on your table:

    ```sql

    ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

    ```

    Now create policies. Start with the principle of least privilege: users can only read their own profile, and only authenticated users can access anything.

    ```sql

    CREATE POLICY "Users can view own profile"

    ON profiles FOR SELECT

    USING (auth.uid() = id);

    CREATE POLICY "Users can update own profile"

    ON profiles FOR UPDATE

    USING (auth.uid() = id);

    CREATE POLICY "Users can insert own profile"

    ON profiles FOR INSERT

    WITH CHECK (auth.uid() = id);

    ```

    That's it. You've now locked your data so only authenticated users see their own records. Everything else fails safely—returning empty results or throwing auth errors instead of exposing data.

    Test this in your local Supabase instance before pushing to production. Try querying as different users. Verify that a user logged in as user_id=123 cannot see user_id=456's data.

    Why Your Next.js Structure Matters for RLS to Work

    RLS only works if you're sending authenticated requests from the server or from a properly authenticated client. This is where most Next.js projects fail.

    Developers often import the Supabase browser client in Server Components:

    ```javascript

    // ❌ WRONG - This will break

    import { createClient } from '@supabase/supabase-js'

    export default async function Page() {

    const supabase = createClient()

    const { data } = await supabase.from('profiles').select()

    return <div>{data}</div>

    }

    ```

    The browser client expects localStorage, cookies, and browser APIs that don't exist on the server. You get hydration errors. RLS doesn't work because the request isn't authenticated.

    The right approach is separating concerns:

  • Create a server-only utility file:
  • ```javascript

    // lib/supabase/server.ts

    import { createClient } from '@supabase/supabase-js'

    import { cookies } from 'next/headers'

    export const createServerClient = async () => {

    const cookieStore = await cookies()

    return createClient(

    process.env.NEXT_PUBLIC_SUPABASE_URL,

    process.env.SUPABASE_SERVICE_ROLE_KEY, // Server-only

    {

    cookies: {

    getAll() {

    return cookieStore.getAll()

    },

    setAll(cookiesToSet) {

    cookiesToSet.forEach(({ name, value, options }) =>

    cookieStore.set(name, value, options)

    )

    },

    },

    }

    )

    }

    ```

  • Create a client utility for browser operations:
  • ```javascript

    // lib/supabase/client.ts

    import { createClient } from '@supabase/supabase-js'

    export const createBrowserClient = () => {

    return createClient(

    process.env.NEXT_PUBLIC_SUPABASE_URL,

    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY // Public, safe

    )

    }

    ```

  • Use server client in Server Components:
  • ```javascript

    // app/dashboard/page.tsx

    import { createServerClient } from '@/lib/supabase/server'

    export default async function Dashboard() {

    const supabase = await createServerClient()

    const { data: profiles } = await supabase.from('profiles').select()

    return <div>{/* render profiles */}</div>

    }

    ```

    This ensures authenticated requests go to Supabase with the user's session, RLS policies work correctly, and you never expose your service role key to the browser.

    How to Use Claude Code (and AI Tools) Without Skipping Security Steps

    This is where many developers cut corners. Claude Code can generate your entire boilerplate in minutes—including RLS setup, project structure, everything. But if you just accept the output without understanding it, you might miss critical security configurations or end up with architecture that doesn't scale.

    Here's how to use Claude Code properly for Supabase + Next.js projects:

  • Give specific, detailed prompts about RLS:
  • "Create a Next.js page that fetches user profiles with Supabase RLS enabled. The user should only see their own profile. Show me the RLS policies, the server client setup, and the Server Component that fetches the data."

  • Verify the generated RLS policies match your requirements:
  • - Is auth.uid() being checked?

    - Are UPDATE/DELETE policies as restrictive as SELECT?

    - Are there any wildcards that let unauthorized access?

  • Test the authentication flow:
  • - Use Claude to generate test cases, not just code

    - Actually run authentication as different users

    - Verify RLS blocks unauthorized access

  • Don't accept the first boilerplate structure:
  • - Review the folder organization

    - Confirm server and client utilities are truly separated

    - Check that the Supabase client isn't imported in the wrong places

    Many developers use ZipBuild specifically to avoid this problem—they get a production-ready scaffold where RLS, authentication middleware, and folder structure are already correct, so they can focus on building features instead of debugging security after launch.

    Testing RLS Before Production

    Always test RLS with real authentication flows:

  • Create two test user accounts locally
  • Log in as user A, fetch data—verify you only see user A's records
  • Log in as user B, fetch data—verify you only see user B's records
  • Try direct SQL queries as an unauthenticated user—verify complete failure
  • Check your browser's Network tab to confirm requests include auth headers
  • If any of these steps show unexpected behavior, you have a policy misconfiguration to fix before production.

    The Real Cost of Skipping Security Structure

    Fixing RLS misconfigurations after launch is exponentially more expensive than getting it right initially. You have to:

  • Audit what data was exposed
  • Notify affected users
  • Migrate data securely
  • Rebuild user trust
  • Spending an extra hour on RLS setup now saves weeks of incident response later.

    The pattern here applies beyond Supabase: whatever security layer you're implementing—auth, permissions, data isolation—configure it first, test it thoroughly, then build features on top. Don't treat it as a nice-to-have for later.

    Try the free discovery chat at zipbuild.dev to see how a properly structured Next.js + Supabase scaffold eliminates these common mistakes from the start.

    Written by ZipBuild Team

    Ready to build with structure?

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

    Start Building