VibeRaven

How do I secure a vibe-coded app?

Close the ways a stranger can reach your data, in order of risk: turn on row level security for every Supabase table with policies that match each row to its owner, keep secret keys out of the browser, verify Stripe webhook signatures, and revoke public access to database functions only your server calls. Then clean secrets out of git, rate limit the routes that cost money, and test it all from a second account. Each step below takes a few lines of SQL or TypeScript.

Why this matters

Vibe-coded apps on Vercel and Supabase tend to share one shape: a browser app that talks to Supabase directly with a public key, a few server routes for payments and AI calls, and secrets in env files. That makes the risks predictable. In the Lovable disclosure tracked as CVE-2025-48757, researchers reported 303 endpoints across 170 projects with inadequate row level security, and UpGuard's 2026 study found 16,326 Supabase databases exposing readable tables. The order below follows impact: the first steps close holes that expose every user's data to anyone, the later ones limit abuse and cost.

Secure it in this order

  • List every public table with row level security off. Supabase's docs say a table in an exposed schema without RLS "is readable and writable by any role with a grant on it", and on projects that still grant anon and authenticated by default, anyone holding your publishable key has that grant:
    select tablename
    from pg_tables
    where schemaname = 'public' and rowsecurity = false;
  • Turn RLS on for each one, and add a policy for each action the app needs that compares the row's owner with the signed-in user. Postgres uses an update policy's using condition as its with check when you leave it out; writing both makes the intent explicit:
    alter table public.notes enable row level security;
    
    create policy "Owners read their notes"
    on public.notes for select
    to authenticated
    using ( (select auth.uid()) = user_id );
    
    create policy "Owners update their notes"
    on public.notes for update
    to authenticated
    using ( (select auth.uid()) = user_id )
    with check ( (select auth.uid()) = user_id );
  • Find policies that let everyone through. A using (true) or with check (true) is fine for public content such as a product catalog, and wrong on anything that belongs to one user:
    select tablename, policyname, cmd, qual, with_check
    from pg_policies
    where schemaname = 'public' and (qual = 'true' or with_check = 'true');
  • Keep secret keys on the server. Supabase's secret or service role key bypasses RLS, and Stripe says its secret key has unrestricted permissions on all its APIs. Neither may sit behind NEXT_PUBLIC_ or VITE_, which Next.js and Vite build into the browser bundle, or in a "use client" file. Search the tracked files, then the build output:
    git grep -n -e SERVICE_ROLE -e sb_secret_ -e sk_live_
    
    # after next build
    grep -rn -e sb_secret_ -e sk_live_ .next/static
  • Verify every Stripe webhook. Read the raw body, pass it with the Stripe-Signature header and your endpoint's whsec_ secret to constructEvent, and reject anything that fails. In a Next.js route handler:
    import Stripe from 'stripe';
    
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
    
    export async function POST(req: Request) {
      const body = await req.text();
      const signature = req.headers.get('stripe-signature') ?? '';
      let event: Stripe.Event;
      try {
        event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
      } catch {
        return new Response('Invalid signature', { status: 400 });
      }
      // handle event.type here
      return new Response('ok');
    }
  • Lock down database functions. By default any role can run a database function, and Supabase warns that a security definer function in an exposed schema "is callable over the Data API with the creator's privileges". List the ones anon can call, then revoke execute and pin the search_path on each one only your server uses:
    select p.oid::regprocedure as function
    from pg_proc p
    join pg_namespace n on n.oid = p.pronamespace
    where n.nspname = 'public'
      and p.prosecdef
      and has_function_privilege('anon', p.oid, 'execute');
    
    revoke execute on function public.delete_all_notes() from public, anon, authenticated;
    alter function public.delete_all_notes() set search_path = '';
  • If a secret was ever committed, rotate it at the provider first, then stop tracking the file. GitHub's docs put rotating first, because a rotated key no longer works, and rewriting history does not reach clones made before it:
    # .gitignore
    .env
    .env*.local
    
    # stop tracking the file, keep your local copy
    git rm --cached .env
  • Rate limit the routes that cost money or send email: AI calls, sign-up, password reset, anything that triggers a paid API. Vercel's WAF has rate limiting rules on all plans, and Supabase Auth enforces rate limits on its own endpoints that you can adjust in the dashboard.
  • Test it from the outside. Call the Data API with only the publishable key: a protected table should return an empty array or a permission error, not rows. Then sign in as a second user and try to read and change the first user's rows:
    curl "https://YOUR_PROJECT.supabase.co/rest/v1/notes?select=*" -H "apikey: YOUR_PUBLISHABLE_KEY"

If something breaks after you lock it down

Turning on RLS usually breaks a query or two the first time. That means it is working; these are the usual causes.

Queries return nothing after RLS is on

With RLS on and no policy for an action, Postgres uses a default-deny policy: no rows are visible or can be modified. Add the policy the app needs for that action and role, scoped to the signed-in user.

Go deeper: Why is my Supabase RLS policy not working?

Inserts fail with "new row violates row-level security policy"

An insert needs its own policy, and its with check has to accept the row your app sends, including the owner column. Make sure the app sets user_id to the signed-in user, or give the column a default of auth.uid().

Go deeper: What is the difference between USING and WITH CHECK in a Supabase RLS policy?

A server route stops working

A route that legitimately acts for no particular user, such as a Stripe webhook that updates a subscription, may need the secret key. Use it only in that server route, never in a client that the browser loads.

Go deeper: Supabase anon key vs service role key: what is the difference?

Optional repo check

For AI-built Vercel + Supabase apps, npx -y viberaven@1.6.3 check is a pre-deploy review for vibe-coded Vercel + Supabase apps that looks for the problems in steps 1 to 8 in your repo files. In runs on a fixture repo on 2026-10-05 it reported a public table without RLS, an update policy with using (true) as a blocker and a select policy with using (true) as a warning, NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY in .env.example, a security definer function with no revoke, a Stripe webhook handler with no signature check, and a .env file not covered by .gitignore. It also flags API routes without rate limiting, though that check currently misses Windows paths: it fired on Linux and not on Windows for the same repo. It did not flag a Stripe secret key hardcoded in a client file, it cannot see your git history, and it sends no requests to your project, so the test in step 9 stays with you. It is advice, not a gate, and a repository check, not a live database or security test. It exits with code 1 when it finds a blocker and writes a .viberaven folder that it does not add to .gitignore.

Related guides

Sources

Is the Supabase anon key in my frontend a security hole?

No. It is meant to be public, and it only reaches what your RLS policies and grants allow. The key that must never reach the browser is the secret or service role key, which bypasses RLS.

Do I need a professional security audit before launch?

For a small app, working through the steps above and testing from a second account covers the holes behind the public incidents on this stack. If you handle payment card data, health data or anything regulated, or business customers ask for one, a review by a security professional is worth paying for.