All articles
Tutorials15 min readDecember 27, 2025
Case StudyLovableJourneyReal World

A Vibe Coder's Security Journey: From Zero to Shipped Safely

Follow along as we take a Lovable-built app from insecure prototype to production-ready deployment.

Security Guide

The Starting Point

You've built an app with Lovable. It works. Users are signing up. Now you need to make sure it's secure.

This is the journey from "it works" to "it's safe."

Day 1: The Security Scan

Running the First Scan

Connected the GitHub repo to ShipReady. Clicked scan. Waited for results.

The Results

Security Score: 34/100

Critical: 3 findings High: 7 findings Medium: 12 findings Low: 8 findings Info: 15 findings

Not great. But fixable.

The Critical Findings

1. SQL Injection in Search

javascript
// src/api/search.ts:15
const results = await db.query(SELECT * FROM products WHERE name LIKE '%${query}%')

2. Hardcoded Supabase Service Key

javascript
// src/lib/supabase.ts:3
const supabase = createClient(url, 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...')

3. Missing Authentication on Admin Route

javascript
// src/app/api/admin/users/route.ts
export async function GET() {
  return Response.json(await db.users.findMany())
}

Reaction

"This is what AI generated? Lovable made SQL injection?"

Yes. AI tools consistently generate these patterns. That's why scanning matters.

Day 2: Fixing Critical Issues

Fix 1: SQL Injection

Before:

javascript
const results = await db.query(
  SELECT * FROM products WHERE name LIKE '%${query}%'
)

After:

javascript
const results = await db.query(
  'SELECT * FROM products WHERE name ILIKE $1',
  [%${query}%]
)

Time: 5 minutes.

Fix 2: Hardcoded Service Key

Before:

javascript
const supabase = createClient(
  'https://xxx.supabase.co',
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
)

After:

  1. Created .env.local:
bash
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
  1. Updated code:
javascript
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
  1. Updated .gitignore:
.env.local
  1. Added to Vercel environment variables.
  1. Rotated the key in Supabase (it was in git history).
Time: 15 minutes.

Fix 3: Missing Admin Authentication

Before:

javascript
export async function GET() {
  return Response.json(await db.users.findMany())
}

After:

javascript
import { getServerSession } from 'next-auth'

export async function GET() { const session = await getServerSession()

if (!session) { return Response.json({ error: 'Unauthorized' }, { status: 401 }) }

if (session.user.role !== 'admin') { return Response.json({ error: 'Forbidden' }, { status: 403 }) }

return Response.json(await db.users.findMany()) }

Time: 10 minutes.

Re-scan After Critical Fixes

Security Score: 58/100 (+24)

Critical: 0 findings ✓ High: 7 findings Medium: 12 findings

Progress.

Day 3: High-Priority Fixes

The High Findings

  1. Missing rate limiting on login
  2. IDOR in order API
  3. XSS in comment display
  4. Weak password requirements
  5. Missing CSRF protection
  6. Session doesn't expire
  7. No HTTPS redirect

Fix 4: IDOR in Orders

Before:

javascript
export async function GET(req, { params }) {
  const order = await db.orders.findUnique({
    where: { id: params.id }
  })
  return Response.json(order)
}

After:

javascript
export async function GET(req, { params }) {
  const session = await getServerSession()

const order = await db.orders.findFirst({ where: { id: params.id, userId: session.user.id } })

if (!order) { return Response.json({ error: 'Not found' }, { status: 404 }) }

return Response.json(order) }

Fix 5: Rate Limiting

Added Upstash rate limiting:

javascript
// middleware.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(5, '15 m'), })

export async function middleware(request) { if (request.nextUrl.pathname === '/api/auth/login') { const ip = request.ip ?? '127.0.0.1' const { success } = await ratelimit.limit(ip)

if (!success) { return new Response('Too Many Requests', { status: 429 }) } }

return NextResponse.next() }

Fix 6: XSS in Comments

Before:

jsx

After:

jsx
{comment.body}

Day 3 Results

Security Score: 78/100 (+20)

Critical: 0 findings ✓ High: 0 findings ✓ Medium: 8 findings

Day 4: Medium Fixes & Security Headers

Adding Security Headers

javascript
// next.config.js
const securityHeaders = [
  {
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload'
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY'
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff'
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin'
  },
]

module.exports = { async headers() { return [{ source: '/:path*', headers: securityHeaders }] }, }

Enabling Supabase RLS

sql
-- Enable RLS on all tables
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

-- Add policies CREATE POLICY "Users see own orders" ON orders FOR SELECT USING (auth.uid() = user_id);

CREATE POLICY "Users manage own profile" ON profiles FOR ALL USING (auth.uid() = id);

Day 4 Results

Security Score: 89/100 (+11)

Critical: 0 findings ✓ High: 0 findings ✓ Medium: 2 findings Low: 5 findings

Day 5: Final Polish

Remaining Medium Findings

  1. No password strength meter on signup
  2. Missing email verification

Fix: Password Validation

typescript
import { z } from 'zod'

const passwordSchema = z.string() .min(8, 'At least 8 characters') .regex(/[A-Z]/, 'Include uppercase') .regex(/[a-z]/, 'Include lowercase') .regex(/[0-9]/, 'Include number')

Fix: Email Verification

Enabled in Supabase Auth settings + added verification check.

Final Scan

Security Score: 94/100

Critical: 0 findings ✓ High: 0 findings ✓ Medium: 0 findings ✓ Low: 3 findings Info: 8 findings

The low findings are suggestions (like adding more CSP rules). The app is production-ready.

The Complete Journey

DayFocusScore
1Initial scan34
2Critical fixes58
3High fixes78
4Security hardening89
5Final polish94

Total time invested: ~8 hours over 5 days.

What We Fixed

Critical Issues Fixed:
✓ SQL injection in search
✓ Hardcoded credentials
✓ Missing admin auth

High Issues Fixed: ✓ IDOR in orders ✓ Rate limiting on login ✓ XSS in comments ✓ Session management

Hardening Added: ✓ Security headers ✓ Row-level security ✓ Input validation ✓ Password requirements

Lessons Learned

1. AI-Generated Code Needs Verification

Lovable is amazing for building fast. It's not amazing at security. That's okay—just scan everything.

2. Most Fixes Are Quick

80% of vulnerabilities took under 10 minutes each to fix. The patterns are well-known.

3. Fixing is Easier Than Breaching

8 hours of security work prevents weeks of breach recovery.

4. Scan Early, Scan Often

Finding these issues at 10 users is way better than at 10,000 users.

The Security Checklist Used

[ ] Run security scan
[ ] Fix all critical findings
[ ] Fix all high findings
[ ] Add security headers
[ ] Enable RLS (if using Supabase)
[ ] Move secrets to environment variables
[ ] Add rate limiting to auth endpoints
[ ] Verify authentication on all protected routes
[ ] Add authorization checks (ownership)
[ ] Validate all user input
[ ] Re-scan to verify fixes

The Bottom Line

Going from 34 to 94 security score took 5 days and about 8 hours of focused work. The app went from "hackable in minutes" to "production-ready."

This is the vibe coder's security journey: build fast with AI, secure with scanning, ship with confidence.

Ready to secure your AI-generated code?

Stop reading about vulnerabilities. Start fixing them.

Start Scanning Free