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/100Critical: 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
// src/api/search.ts:15
const results = await db.query(SELECT * FROM products WHERE name LIKE '%${query}%')2. Hardcoded Supabase Service Key
// src/lib/supabase.ts:3
const supabase = createClient(url, 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...')3. Missing Authentication on Admin Route
// 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:
const results = await db.query(
SELECT * FROM products WHERE name LIKE '%${query}%'
)After:
const results = await db.query(
'SELECT * FROM products WHERE name ILIKE $1',
[%${query}%]
)Time: 5 minutes.
Fix 2: Hardcoded Service Key
Before:
const supabase = createClient(
'https://xxx.supabase.co',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
)After:
- Created .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...- Updated code:
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)- Updated .gitignore:
.env.local- Added to Vercel environment variables.
- Rotated the key in Supabase (it was in git history).
Fix 3: Missing Admin Authentication
Before:
export async function GET() {
return Response.json(await db.users.findMany())
}After:
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
- Missing rate limiting on login
- IDOR in order API
- XSS in comment display
- Weak password requirements
- Missing CSRF protection
- Session doesn't expire
- No HTTPS redirect
Fix 4: IDOR in Orders
Before:
export async function GET(req, { params }) {
const order = await db.orders.findUnique({
where: { id: params.id }
})
return Response.json(order)
}After:
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:
// 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:
After:
{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
// 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
-- 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
- No password strength meter on signup
- Missing email verification
Fix: Password Validation
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/100Critical: 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
| Day | Focus | Score |
|---|
| 1 | Initial scan | 34 |
|---|
| 2 | Critical fixes | 58 |
|---|
| 3 | High fixes | 78 |
|---|
| 4 | Security hardening | 89 |
|---|
| 5 | Final polish | 94 |
|---|
Total time invested: ~8 hours over 5 days.
What We Fixed
Critical Issues Fixed:
✓ SQL injection in search
✓ Hardcoded credentials
✓ Missing admin authHigh 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 fixesThe 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.