← All posts
supabase securityvibe codingRLSAI coding toolsdatabase security

How to Secure Your Supabase Database in a Vibe-Coded App

O

OverMCP Team

Quick answer

To secure your Supabase database in a vibe-coded app, enable Row Level Security (RLS) on every table, define strict policies that validate user identity and input, and never expose your service_role key client-side. Most AI tools generate Supabase code with RLS disabled or with permissive policies, leaving your data open to unauthorized access.

What to check first

Before you deploy, run through this checklist to catch the most common Supabase database security issues in AI-generated code:

  • [ ] RLS enabled on all tables? Go to the Supabase dashboard -> Authentication -> Policies. If you see "RLS not enabled" on any table, turn it on immediately.
  • [ ] Service_role key usage? Search your codebase for service_role or SUPABASE_SERVICE_ROLE_KEY. This key should never appear in client-side code or public repositories.
  • [ ] Policies defined? For each table with RLS enabled, verify that at least one policy exists. Empty RLS = all queries denied.
  • [ ] Policy checks user identity? Ensure policies use auth.uid() or auth.role() to restrict access to authenticated users.
  • [ ] Input sanitization? Check that policies escape or validate user-supplied values (e.g., using quote_literal or parameterized queries).
  • [ ] Exposed schema? In the SQL editor, run SELECT * FROM information_schema.tables to see if any internal tables are accidentally public.
  • Step-by-step fix

    Let's fix a common scenario: an AI-generated Next.js app with Supabase that stores user profiles. The AI likely created code like this:

    // ❌ Unsafe: no RLS, uses service_role key client-side
    import { createClient } from '@supabase/supabase-js'
    
    const supabase = createClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL,
      process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY // 🚨 exposed!
    )
    
    // Fetch all profiles – no access control
    export async function getProfiles() {
      const { data } = await supabase.from('profiles').select('*')
      return data
    }

    Step 1: Remove service_role key from client code

    Create a server-only client using the @supabase/ssr package or Next.js route handlers:

    // lib/supabaseServer.js – server-only
    import { createClient } from '@supabase/supabase-js'
    
    const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
    const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
    
    // Only use this in API routes or server components
    export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)

    For client components, use the anon (public) key with RLS:

    // lib/supabaseClient.js
    import { createBrowserClient } from '@supabase/ssr'
    
    export function createClient() {
      return createBrowserClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
      )
    }

    Step 2: Enable RLS on the profiles table

    In Supabase SQL editor:

    ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

    Step 3: Create a policy for authenticated users

    -- Allow users to view only their own profile
    CREATE POLICY "Users can view own profile"
    ON profiles FOR SELECT
    USING (auth.uid() = id);
    
    -- Allow users to update only their own profile
    CREATE POLICY "Users can update own profile"
    ON profiles FOR UPDATE
    USING (auth.uid() = id)
    WITH CHECK (auth.uid() = id);
    
    -- Allow users to insert their own profile during signup
    CREATE POLICY "Users can insert own profile"
    ON profiles FOR INSERT
    WITH CHECK (auth.uid() = id);

    Step 4: Update client code to use RLS

    // pages/api/profile.js (or app/api/profile/route.ts)
    import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
    import { cookies } from 'next/headers'
    
    export async function GET(request) {
      const supabase = createRouteHandlerClient({ cookies })
      const { data: { user } } = await supabase.auth.getUser()
    
      if (!user) return new Response('Unauthorized', { status: 401 })
    
      const { data: profile } = await supabase
        .from('profiles')
        .select('*')
        .eq('id', user.id)
        .single()
    
      return Response.json(profile)
    }

    Common mistakes

  • Using the service_role key in the browser. AI tools often copy-paste the service_role key into environment variables prefixed with NEXT_PUBLIC_. This key bypasses all RLS – anyone who finds it can read, write, or delete any data.
  • RLS enabled but no policies defined. This locks down the table completely, causing silent failures. AI-generated code often enables RLS but forgets to create policies, breaking features without clear error messages.
  • Policies that don't check user identity. For example, a policy that allows SELECT * FROM profiles without filtering by auth.uid() exposes every user's data.
  • Storing secrets in .env files committed to GitHub. Vibe-coded apps frequently commit .env files. Use a secret leak scanner to catch this before it's too late.
  • Not using prepared statements in SQL functions. If you write raw SQL in Supabase Edge Functions or triggers, concatenating user input can lead to SQL injection. Use quote_literal or parameterized queries.
  • How to prevent Supabase database vulnerabilities in future projects

    To avoid these issues from the start, integrate security checks into your workflow:

  • Use a [free AI app security scanner](https://www.overmcp.com/) to scan your vibe-coded app for exposed secrets, missing RLS, and other common issues before deployment.
  • Set up [continuous security monitoring](https://www.overmcp.com/monitor) to get alerted when new vulnerabilities appear in your Supabase configuration.
  • Review your Supabase policies regularly – especially after AI-generated schema changes or new features.
  • FAQ

    What is the biggest Supabase security mistake in vibe-coded apps?

    The biggest mistake is leaving Row Level Security (RLS) disabled on tables that contain user data. AI coding tools often generate code that works without RLS for simplicity, but this means any authenticated user (or anyone with the anon key) can access all records.

    How do I find if my Supabase service_role key is exposed?

    Search your entire codebase (including Git history) for the key pattern sbp_ or service_role. Also check your .env file and any client-side JavaScript bundles. Use a secret leak scanner to automatically detect exposed keys.

    Can I use Supabase RLS policies with Next.js App Router?

    Yes. Use the @supabase/ssr package to create a server client that respects the user's session from cookies. Define RLS policies as shown above, and use the anon key client-side – RLS will enforce access control on every query.

    Is your app secure?

    Free scan in 30 seconds. No signup needed.

    Scan My App Free