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:
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:
```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)
)
},
},
}
)
}
```
```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
)
}
```
```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:
"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."
- Is auth.uid() being checked?
- Are UPDATE/DELETE policies as restrictive as SELECT?
- Are there any wildcards that let unauthorized access?
- Use Claude to generate test cases, not just code
- Actually run authentication as different users
- Verify RLS blocks unauthorized access
- 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:
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:
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