This is the point where vibe coding stops being risk-free. Up to here, the worst case was a broken page. From here, the worst case is a stranger reading your users’ data.
The good news is the failure is specific and predictable. Here’s what goes wrong and exactly how to catch it.
Don’t Let AI Invent Auth
First rule: use an established provider. Ask for Supabase Auth, Clerk, Auth.js, or Better Auth by name.
Password hashing, session rotation, timing-safe comparison, token expiry — these are solved, and the mistakes are subtle and silent. An AI-written auth system will work fine in testing and fail in ways you won’t discover until it matters.
Add authentication using Supabase Auth. Email and password plus Google OAuth. Include sign up, sign in, sign out, and password reset. Protect the /dashboard route so unauthenticated users are redirected to /login. Use the server client for session checks, not client-side only.
That last sentence prevents a specific bug: client-side-only route protection, where the “protected” page is fully readable by anyone who disables JavaScript or reads the network response.
The Gap That Matters
Here’s what will be wrong. Not might be — will be.
Your app has projects. Users should see their own. The AI writes:
// app/api/projects/[id]/route.ts
export async function GET(req, { params }) {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return new Response('Unauthorized', { status: 401 })
const { data } = await supabase
.from('projects')
.select('*')
.eq('id', params.id) // ← filters by project, not by owner
.single()
return Response.json(data)
}
This checks you’re logged in. It does not check the project is yours. Any authenticated user can read any project by changing the number in the URL.
The app works perfectly. Every feature functions. Every user can read every other user’s data.
The fix is one clause:
.eq('id', params.id)
.eq('user_id', user.id) // ← the line that was missing
Ask For It Explicitly
Every query that reads or writes user-owned data must filter by the authenticated user’s ID at the database level — not in the UI, not after fetching. Go through every route handler and show me each place this is enforced. List any that don’t.
Then read the list yourself. Don’t take “all good” as an answer without seeing the queries.
Enable Row-Level Security
If you’re on Supabase, this is the safety net, and it’s frequently left off because enabling it mid-build causes errors.
Your Supabase anon key is public — it ships to the browser by design. What stops someone querying your entire database from their console is RLS. Without it, your tables are open.
Enable row-level security on every table. Write policies so authenticated users can only select, insert, update and delete rows where user_id matches auth.uid(). Show me the SQL for each policy.
Check it yourself: Supabase dashboard → Table Editor → look for the RLS badge on every single table. No badge means no protection.
The Service Role Key
Supabase gives you a service_role key that bypasses all RLS. It belongs on the server, in an environment variable without the NEXT_PUBLIC_ prefix, and nowhere else.
AI tools reach for it when RLS policies block something, because it makes the error go away. That “fix” removes all your access control at once.
grep -rn "service_role\|SERVICE_ROLE" --include="*.ts" --include="*.tsx" .
Every result should be in server-only code. If any is in a component that renders in the browser, rotate the key immediately.
Test It Properly
Not optional, and it takes five minutes.
- Create account A. Make a record. Note its ID.
- Create account B in a different browser or private window.
- As B, visit
/projects/[A's id]directly. - As B, call
/api/projects/[A's id]directly. - Log out entirely. Call the API again.
All three should fail. If any returns data, you have the vulnerability.
Then check the database layer:
Give me a curl command that queries the projects table directly using the public anon key, without any auth token.
Run it. It should return nothing.
Other Things That Get Skipped
Email verification. Ask for it explicitly or anyone can sign up as anyone.
Rate limiting on login. Without it, password guessing is unlimited.
Add rate limiting to sign in and password reset — 5 attempts per email per 15 minutes.
Server-side session checks. A useEffect that redirects unauthenticated users is a suggestion, not a control. The data was already sent.
Sensible error messages. “No account with that email” tells an attacker which addresses are registered. Keep login errors generic.
The Checklist
- Using an established auth provider, not custom code
- Route protection enforced server-side
- Every user-data query filters by authenticated user ID
- RLS enabled on every table, policies written
- Service role key server-side only
- Two-account test passed on both page and API routes
- Logged-out API test returns 401
- Direct database query with anon key returns nothing
- Email verification on
- Rate limiting on auth endpoints
- Login errors don’t reveal which emails exist
The Honest Framing
Everything above is one afternoon. The vulnerability it prevents is the difference between a side project and a data breach involving other people.
You can vibe code the entire rest of your app. This part you read.
Next: Vibe code a CRUD app applies these patterns to real data operations, and vibe coding security covers the full checklist across every layer.
Related reading: