{"slug": "how-to-harden-vibe-coded-next-js-15-bolt-new-apps-for-production-supabase-rls", "title": "How to Harden Vibe-Coded Next.js 15 & Bolt.new Apps for Production (Supabase RLS + Stripe Webhooks)", "summary": "A developer published a 30-minute hardening checklist for taking AI-generated Next.js 15 and Bolt.new apps to production, covering Supabase row-level security policies, Stripe webhook signature verification with idempotent order fulfillment, and Zod input validation. The guide argues that vibe-coding tools like Bolt.new and Cursor optimize for speed to demo rather than production readiness, leaving RLS disabled and webhooks unconfigured by default.", "body_md": "You shipped fast. Bolt.new gave you a working app in 20 minutes. Cursor wrote the features while you slept. The demo works.\n\nThen you check Supabase: **RLS is disabled on all tables**. Stripe webhooks? **Not configured**. Your users' data and payments are exposed.\n\nThis 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.\n\nBolt.new and Cursor optimize for **speed to demo**, not **production readiness**. They'll give you:\n\nBut they skip:\n\n```\n-- 1. Users only see their own data\nCREATE POLICY \"Users can view own data\" ON public.profiles\n  FOR SELECT USING (auth.uid() = id);\n\n-- 2. Users only update their own profile\nCREATE POLICY \"Users can update own profile\" ON public.profiles\n  FOR UPDATE USING (auth.uid() = id);\n\n-- 3. Orders: users see only their orders\nCREATE POLICY \"Users can view own orders\" ON public.orders\n  FOR SELECT USING (auth.uid() = user_id);\n\n-- 4. Order items: only via orders they own\nCREATE POLICY \"Users can view own order items\" ON public.order_items\n  FOR SELECT USING (\n    EXISTS (\n      SELECT 1 FROM public.orders o\n      WHERE o.id = order_items.order_id AND o.user_id = auth.uid()\n    )\n  );\n\n-- 5. Admins bypass (optional, for support)\nCREATE POLICY \"Admins full access\" ON public.profiles\n  FOR ALL USING (\n    EXISTS (\n      SELECT 1 FROM public.profiles p\n      WHERE p.id = auth.uid() AND p.role = 'admin'\n    )\n  );\n```\n\n**Test it:** Sign in as User A, try to fetch User B's data. Should return empty.\n\n``` js\n// app/api/webhooks/stripe/route.ts\nimport { headers } from 'next/headers';\nimport { stripe } from '@/lib/stripe';\nimport { createClient } from '@supabase/supabase-js';\n\nexport async function POST(req: Request) {\n  const body = await req.text();\n  const signature = headers().get('stripe-signature')!;\n\n  let event;\n\n  try {\n    event = stripe.webhooks.constructEvent(\n      body,\n      signature,\n      process.env.STRIPE_WEBHOOK_SECRET!\n    );\n  } catch (err) {\n    console.error('Webhook signature verification failed:', err);\n    return new Response('Webhook Error', { status: 400 });\n  }\n\n  // Handle the event\n  switch (event.type) {\n    case 'checkout.session.completed': {\n      const session = event.data.object as Stripe.Checkout.Session;\n      await fulfillOrder(session);\n      break;\n    }\n    case 'payment_intent.payment_failed': {\n      const paymentIntent = event.data.object as Stripe.PaymentIntent;\n      await handleFailedPayment(paymentIntent);\n      break;\n    }\n    default:\n      console.log(`Unhandled event type: ${event.type}`);\n  }\n\n  return new Response(null, { status: 200 });\n}\n\nasync function fulfillOrder(session: Stripe.Checkout.Session) {\n  const supabase = createClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.SUPABASE_SERVICE_ROLE_KEY!\n  );\n\n  // Idempotency: check if already processed\n  const { data: existing } = await supabase\n    .from('orders')\n    .select('id')\n    .eq('stripe_session_id', session.id)\n    .single();\n\n  if (existing) return; // Already processed\n\n  // Create order with RLS-safe service role\n  await supabase.from('orders').insert({\n    user_id: session.metadata?.user_id,\n    stripe_session_id: session.id,\n    amount_total: session.amount_total,\n    currency: session.currency,\n    status: 'paid',\n    created_at: new Date().toISOString()\n  });\n}\n```\n\n**Key points:**\n\n`stripe-signature` header`STRIPE_WEBHOOK_SECRET` from Stripe Dashboard`stripe_session_id` before insert)\n\n``` js\n// lib/validators.ts\nimport { z } from 'zod';\n\nexport const createOrderSchema = z.object({\n  items: z.array(z.object({\n    product_id: z.string().uuid(),\n    quantity: z.number().int().positive().max(99),\n  })).min(1).max(50),\n  shipping_address: z.object({\n    name: z.string().min(1).max(100),\n    phone: z.string().regex(/^\\+?[0-9\\s-]{10,15}$/),\n    address_line1: z.string().min(5).max(200),\n    city: z.string().min(1).max(100),\n    postal_code: z.string().regex(/^[0-9]{5}$/),\n    country: z.string().length(2).default('ID'),\n  }),\n});\n\n// In your API route:\nexport async function POST(req: Request) {\n  const body = await req.json();\n  const parsed = createOrderSchema.safeParse(body);\n\n  if (!parsed.success) {\n    return Response.json({ errors: parsed.error.flatten() }, { status: 400 });\n  }\n\n  // Proceed with validated data\n}\n```\n\n| Variable | Required | Notes | \n|---|---|---|\n| `NEXT_PUBLIC_SUPABASE_URL` | ✅ | Public, safe in client | \n| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | ✅ | Public, RLS enforced | \n| `SUPABASE_SERVICE_ROLE_KEY` | ✅ | **Secret!** Server only, bypasses RLS | \n| `STRIPE_SECRET_KEY` | ✅ | **Secret!** Server only | \n| `STRIPE_WEBHOOK_SECRET` | ✅ | **Secret!** From Stripe Dashboard | \n| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | ✅ | Public, safe in client | \n\n**Never commit `.env.local`.** Use Vercel/Netlify environment variables.\n\n```\n[ ] RLS enabled on ALL tables with policies\n[ ] Test: User A cannot read User B data\n[ ] Stripe webhook endpoint deployed & verified\n[ ] Webhook signature verification implemented\n[ ] Idempotency keys on all payment events\n[ ] Server-side validation (Zod) on all mutations\n[ ] Service Role Key ONLY in server code\n[ ] No secrets in client bundle (check network tab)\n[ ] Error logging (Sentry/LogRocket) configured\n[ ] Stripe test mode → live mode switch verified\n```\n\nI packaged the complete implementation: RLS migration files, webhook handlers, Zod validators, environment template, and a test script that verifies your hardening in one command.\n\n**Gumroad ($14.99 → $10.49 with code `VIBE30`):**\n\n[https://ancuboy.gumroad.com/l/cursor-bolt-production-hardening-pack](https://ancuboy.gumroad.com/l/cursor-bolt-production-hardening-pack)", "url": "https://wpnews.pro/news/how-to-harden-vibe-coded-next-js-15-bolt-new-apps-for-production-supabase-rls", "canonical_source": "https://dev.to/housharenet/how-to-harden-vibe-coded-nextjs-15-boltnew-apps-for-production-supabase-rls-stripe-webhooks-25op", "published_at": "2026-10-11 02:10:57+00:00", "updated_at": "2026-10-11 02:19:51.095108+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-products"], "entities": ["Bolt.new", "Next.js", "Supabase", "Stripe", "Cursor", "Zod"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-to-harden-vibe-coded-next-js-15-bolt-new-apps-for-production-supabase-rls", "markdown": "https://wpnews.pro/news/how-to-harden-vibe-coded-next-js-15-bolt-new-apps-for-production-supabase-rls.md", "text": "https://wpnews.pro/news/how-to-harden-vibe-coded-next-js-15-bolt-new-apps-for-production-supabase-rls.txt", "jsonld": "https://wpnews.pro/news/how-to-harden-vibe-coded-next-js-15-bolt-new-apps-for-production-supabase-rls.jsonld"}}