DEV Community

Ryan Cole
Ryan Cole

Posted on

How to Harden Vibe-Coded Next.js 15 & Bolt.new Apps for Production (Supabase RLS + Stripe Webhooks)

How to Harden Vibe-Coded Next.js 15 & Bolt.new Apps for Production (Supabase RLS + Stripe Webhooks)

You shipped fast. Bolt.new gave you a working app in 20 minutes. Cursor wrote the features while you slept. The demo works.

Then you check Supabase: RLS is disabled on all tables. Stripe webhooks? Not configured. Your users' data and payments are exposed.

This is the "vibe coding" trap: AI generates features, not production guardrails. Here's the 30-minute hardening checklist I use before any AI-built app goes live.


The Problem: AI Codes Features, Not Boundaries

Bolt.new and Cursor optimize for speed to demo, not production readiness. They'll give you:

  • Working auth flows
  • Database schemas
  • Payment integration UI

But they skip:

  • Row Level Security policies (RLS off by default in Supabase)
  • Stripe webhook signature verification
  • Idempotency keys for payment retries
  • Server-side validation (they trust the client)

1. Supabase RLS: The 5 Policies Every App Needs

-- 1. Users only see their own data
CREATE POLICY "Users can view own data" ON public.profiles
  FOR SELECT USING (auth.uid() = id);

-- 2. Users only update their own profile
CREATE POLICY "Users can update own profile" ON public.profiles
  FOR UPDATE USING (auth.uid() = id);

-- 3. Orders: users see only their orders
CREATE POLICY "Users can view own orders" ON public.orders
  FOR SELECT USING (auth.uid() = user_id);

-- 4. Order items: only via orders they own
CREATE POLICY "Users can view own order items" ON public.order_items
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM public.orders o
      WHERE o.id = order_items.order_id AND o.user_id = auth.uid()
    )
  );

-- 5. Admins bypass (optional, for support)
CREATE POLICY "Admins full access" ON public.profiles
  FOR ALL USING (
    EXISTS (
      SELECT 1 FROM public.profiles p
      WHERE p.id = auth.uid() AND p.role = 'admin'
    )
  );
Enter fullscreen mode Exit fullscreen mode

Test it: Sign in as User A, try to fetch User B's data. Should return empty.


2. Stripe Webhooks: Verify Every Event

// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { createClient } from '@supabase/supabase-js';

export async function POST(req: Request) {
  const body = await req.text();
  const signature = headers().get('stripe-signature')!;

  let event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    return new Response('Webhook Error', { status: 400 });
  }

  // Handle the event
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session;
      await fulfillOrder(session);
      break;
    }
    case 'payment_intent.payment_failed': {
      const paymentIntent = event.data.object as Stripe.PaymentIntent;
      await handleFailedPayment(paymentIntent);
      break;
    }
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  return new Response(null, { status: 200 });
}

async function fulfillOrder(session: Stripe.Checkout.Session) {
  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  );

  // Idempotency: check if already processed
  const { data: existing } = await supabase
    .from('orders')
    .select('id')
    .eq('stripe_session_id', session.id)
    .single();

  if (existing) return; // Already processed

  // Create order with RLS-safe service role
  await supabase.from('orders').insert({
    user_id: session.metadata?.user_id,
    stripe_session_id: session.id,
    amount_total: session.amount_total,
    currency: session.currency,
    status: 'paid',
    created_at: new Date().toISOString()
  });
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Always verify stripe-signature header
  • Use STRIPE_WEBHOOK_SECRET from Stripe Dashboard
  • Implement idempotency (check stripe_session_id before insert)
  • Use Service Role Key for server-side writes (bypasses RLS safely)

3. Server-Side Validation: Never Trust the Client

// lib/validators.ts
import { z } from 'zod';

export const createOrderSchema = z.object({
  items: z.array(z.object({
    product_id: z.string().uuid(),
    quantity: z.number().int().positive().max(99),
  })).min(1).max(50),
  shipping_address: z.object({
    name: z.string().min(1).max(100),
    phone: z.string().regex(/^\+?[0-9\s-]{10,15}$/),
    address_line1: z.string().min(5).max(200),
    city: z.string().min(1).max(100),
    postal_code: z.string().regex(/^[0-9]{5}$/),
    country: z.string().length(2).default('ID'),
  }),
});

// In your API route:
export async function POST(req: Request) {
  const body = await req.json();
  const parsed = createOrderSchema.safeParse(body);

  if (!parsed.success) {
    return Response.json({ errors: parsed.error.flatten() }, { status: 400 });
  }

  // Proceed with validated data
}
Enter fullscreen mode Exit fullscreen mode

4. Environment Hardening Checklist

Variable Required Notes
NEXT_PUBLIC_SUPABASE_URL ✅ Public, safe in client
NEXT_PUBLIC_SUPABASE_ANON_KEY ✅ Public, RLS enforced
SUPABASE_SERVICE_ROLE_KEY ✅ Secret! Server only, bypasses RLS
STRIPE_SECRET_KEY ✅ Secret! Server only
STRIPE_WEBHOOK_SECRET ✅ Secret! From Stripe Dashboard
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ✅ Public, safe in client

Never commit .env.local. Use Vercel/Netlify environment variables.


5. The "Ship Tonight" Checklist (Copy-Paste)

[ ] RLS enabled on ALL tables with policies
[ ] Test: User A cannot read User B data
[ ] Stripe webhook endpoint deployed & verified
[ ] Webhook signature verification implemented
[ ] Idempotency keys on all payment events
[ ] Server-side validation (Zod) on all mutations
[ ] Service Role Key ONLY in server code
[ ] No secrets in client bundle (check network tab)
[ ] Error logging (Sentry/LogRocket) configured
[ ] Stripe test mode → live mode switch verified
Enter fullscreen mode Exit fullscreen mode

The Pack: All of This, Pre-Built

I packaged the complete implementation: RLS migration files, webhook handlers, Zod validators, environment template, and a test script that verifies your hardening in one command.

Gumroad ($14.99 → $10.49 with code VIBE30):

https://ancuboy.gumroad.com/l/cursor-bolt-production-hardening-pack


Top comments (0)