I was halfway through an interview process for a standard full-stack engineering role at a company here in Prague, so I took a look at their recently launched product as part of my research. They'd built the low-code app quickly to capture a market opportunity.
But within five minutes, I had access to their entire database: over 2,000 customer records. Names, emails, payment information, chat messages. Everything.
I reported it because I was concerned and wanted to help, but they got defensive. Really defensive. Instead of thanking me for the heads-up, they treated me like I'd done something wrong.
It happens more often than you'd think, but the most serious version of the problem takes five minutes to check for yourself.
The five-minute database check
Before you hire anyone to review your vibe-coded app, check for the most critical issue yourself. It takes five minutes and zero technical skill.
Check your own app
If your app uses Supabase (and most vibe-coded apps do), check what an attacker could reach:
- Open your app in Chrome and press F12 (or right-click and choose Inspect).
- Go to the Network tab.
- Use your app normally: log in, click around, load some data.
- Filter the requests by typing "supabase". You're looking for requests to URLs like
xxxxx.supabase.co. - Click any Supabase request and find the request headers. Look for one called
apikey. On older projects it's a long string starting witheyJ; on newer ones it starts withsb_publishable_. - Copy that key. It's your public key (Supabase calls the new version the
publishable key, and the legacy one is the anon key). It's supposed to be public, so that's not the problem. The problem is what that key can reach.
- Test what's exposed. Take your Supabase URL and that key to
Supabase Schema, paste them in and click Fetch. You'll see every table in your database. Try querying each table directly. If row-level security isn't configured properly, you'll get back all the data, not just the current user's.
What the schema tells you
| Red flag | Why it matters |
|---|---|
| Tables with no relationships | Orphaned data that's probably insecure |
No created_at or updated_at columns | No audit trail |
| Sensitive data stored as plain text | Passwords and API keys readable by anyone who gets in |
No user_id foreign key on user-owned data | No way to restrict access |
Tables named test, temp or backup | Forgotten development artefacts |
The good signs are clear naming conventions, proper UUID primary keys, defined foreign key relationships and timestamp columns for an audit trail.
What vibe coding actually produces
When you build with AI tools (Cursor, Lovable,
Bolt, Replit, v0), you're asking an AI to generate code from your prompts. But the AI doesn't understand your business logic, security requirements or scaling needs. It understands patterns from training data, so it optimises for code that works today rather than code that can be maintained.
The result is code that works until it doesn't, often hiding problems under the hood:
- Inconsistent patterns: each prompt might generate code following different conventions
- Security gaps: API keys in frontend code, missing authentication checks, overly permissive database rules
- No error handling: the happy path works, edge cases crash
- Performance landmines: queries that work with 10 users but break with 1,000
- Accumulated complexity: features stacked on features with no refactoring
None of it feels like spaghetti while you're building because every feature works and users are happy. But each addition sits on a more fragile foundation, so complexity multiplies until one day you can't ship any more. Nested conditionals pile up:
// What AI generates: works today, breaks tomorrow
if (user.type === "premium") {
if (feature === "export") {
if (format === "pdf") {
// 50 more nested ifs...
}
}
}
// What production needs: a pattern that scales
const exportStrategy = exportStrategies[user.tier][format];
return exportStrategy.execute(data);
These issues are fixable, but you need to know what you're dealing with.
The review checklist
A proper review of a vibe-coded app covers different ground from a typical code review. Much of it maps onto the OWASP Top 10, where broken access control sits at number one.
Authentication and authorisation
| Check | What to look for |
|---|---|
| Auth implementation | Is Supabase Auth (or similar) properly configured? |
| Route protection | Are authenticated routes actually protected? |
| Role-based access | If you have admin features, are they properly gated? |
| Token handling | Are JWTs stored securely (not localStorage for sensitive apps)? |
| Session management | Do sessions expire appropriately? |
Database security (RLS)
Row-level security is where most vibe-coded apps fail. In Supabase's own words, a table in an exposed schema without RLS is readable and writable by any role with a grant on it.
| Check | What to look for |
|---|---|
| RLS enabled | Is RLS actually turned on for each table? |
| Policy coverage | Do policies exist for SELECT, INSERT, UPDATE and DELETE? |
| Policy logic | Can users only access their own data? |
| Admin bypass | Is there a proper service role pattern for admin functions? |
API keys and environment variables
| Check | What to look for |
|---|---|
| Key exposure | Are API keys hardcoded in frontend code? |
| Environment separation | Different keys for development, staging and production? |
| Secret handling | Sensitive keys only on the backend or in serverless functions? |
| Rate limiting | Any protection against API abuse? |
Frontend and input security
| Check | What to look for |
|---|---|
| Input validation | Are user inputs sanitised? |
| Injection | Are database queries parameterised, so input can't become SQL? |
| XSS protection | Can users inject scripts through form fields? |
| CORS configuration | Is it properly restricted, or wide open (*)? |
| Error messages | Do errors expose internal details to users? |
Code quality
| Check | What to look for |
|---|---|
| Consistent patterns | The same approach throughout, or chaos? |
| Error handling | Try/catch blocks, error boundaries? |
| Type safety | TypeScript types actually defined, or any everywhere? |
| Dead code | Unused components, commented-out blocks? |
Auditing Supabase from the dashboard
If you're hiring a reviewer, expect them to check Supabase's security settings:
- RLS status. Open the Table Editor and check each table's RLS indicator. If RLS shows as disabled, that's bad.
- Policies. Go to Authentication, then Policies. Every table should have policies defined, and they should reference
auth.uid()to restrict rows to the current user. - Function permissions. Go to Database, then Functions, and look for
SECURITY DEFINERfunctions. They run with elevated privileges, so a poorly configured onecan return rows the caller isn't allowed to read. Each needs careful review.
- Storage buckets. Go to Storage and check the bucket policies. Are uploaded files public when they shouldn't be?
Three issues AI-generated code keeps shipping
1. The "works locally" problem. AI tools often generate code that works in development but exposes secrets in production:
// Bad: API key in frontend
const stripe = new Stripe('sk_live_xxxxx')
// Good: key only on the server
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
2. Missing server-side validation. Just because the frontend validates doesn't mean the API does. Check that every API endpoint validates its input, that database constraints exist as a last line of defence, and that no business logic relies solely on frontend checks.
3. Over-privileged database access. Check whether the app uses the service role key (now called the secret key) from the frontend. It shouldn't: that key bypasses row-level security entirely.
Production readiness
Whether you're preparing to launch or already live, check your app's production readiness.
Environment and operations
| Item | Production requirement |
|---|---|
| Environment variables | All secrets in env vars, not code |
| Build configuration | Production builds enabled (no dev mode) |
| Error tracking | Sentry or similar configured |
| Logging | Structured logs for debugging |
| Monitoring | Uptime and performance monitoring in place |
| Deployments | Automated, with a tested rollback procedure |
| Incidents | A plan for who does what when something breaks |
Performance baseline
| Check | Why it matters |
|---|---|
| Database indexes | Slow queries will kill you at scale |
| Image optimisation | Large images destroy load times |
| Bundle size | AI tools often import entire libraries |
| Caching and CDN | Static assets and expensive operations should be cached |
| Background jobs | Slow work belongs in a queue, not the request |
| API response time | Aim for under 200ms |
Backup and recovery
| Requirement | Status needed |
|---|---|
| Database backups | Automated daily backups |
| Point-in-time recovery | Ability to restore to a specific moment |
| Backup testing | Actually tested restoring from a backup |
On Supabase, daily backups come with the paid plans, so free projects need their own exports. Point-in-time recovery is an add-on.
Developer experience
| Check | What to look for |
|---|---|
| Local development | Can a new developer run it locally? |
| TypeScript | Strict mode on |
| Tests | An automated test suite |
| Formatting | Prettier and ESLint, enforced by Git hooks |
| Structure | A clear folder structure and documented components |
Getting from prototype to production
It's time to move when you have paying users, performance problems or developers to onboard. The prototype that got you there won't get you further, so the work runs in roughly this order.
- Triage. Audit the codebase for critical vulnerabilities, find the breaking points and performance bottlenecks, map feature dependencies and technical debt, and prioritise fixes by user impact.
- Foundations. Establish a proper project structure (a monorepo when appropriate), consistent patterns across the codebase, essential infrastructure (error handling, logging, monitoring), and shared utilities and components.
- Rules for the AI. Instead of letting AI generate whatever it wants, write strict guidelines it has to follow: one way to fetch data, one place for state, one API client with retry logic, feature folders, no inline styles. Enforce them with pre-commit hooks: no direct API calls in components, no conditionals nested more than three deep, TypeScript strict mode.
- Pull the backend out of the frontend. Most no-code platforms create frontend-heavy applications with business logic scattered through components. Move that logic into a proper API layer (Hono suits lightweight needs) with clear data contracts, then into services, before adding database indexing, queues for background jobs, caching and rate limiting.
- Handoff. Write the documentation any developer can pick up: an architecture overview, the reasoning behind decisions, pattern examples and anti-patterns, recorded walkthroughs and an onboarding checklist.
Payment logic belongs in a service:
// Before: logic scattered across components
const handlePayment = () => {
// 200 lines of payment logic in a React component
};
// After: a service does the work
const handlePayment = () => {
return paymentService.process(order);
};
It's tempting to keep building features in the no-code tool while engineers rebuild underneath it. I tried exactly that on Snacker.ai, but it cost more than it saved. From Lovable to production is the post-mortem on why. The tools still earn their place for the right work:
| Keep using no-code for | Use production engineering for |
|---|---|
| Rapid prototyping | Core business logic |
| UI and UX iterations | Performance-critical paths |
| Marketing pages | Security-sensitive features |
| Admin interfaces | Complex integrations |
| Internal tools | Scalable infrastructure |
Who to hire
You need a senior full-stack developer with specific experience.
Must-haves:
- 5+ years of professional development experience
- Production Supabase experience (or the equivalent: Firebase, Postgres)
- A security background: has performed security audits before
- AI-code experience: has cleaned up vibe-coded apps before
Nice-to-haves: experience with your specific stack (Next.js, React and so on), DevOps and deployment experience, and time spent with early-stage startups.
Questions to ask candidates
To verify technical skill:
- "Walk me through how you'd audit RLS policies in Supabase."
- "What are the top three security issues you see in AI-generated code?"
- "How would you handle finding API keys exposed in the frontend?"
To verify experience:
- "Tell me about a vibe-coded app you've reviewed. What did you find?"
- "What's your process for a code security audit?"
- "How do you prioritise which issues to fix first?"
To check communication:
- "How would you explain a critical security issue to a non-technical founder?"
- "What documentation would you provide after the review?"
Red flags in candidates
- Can't explain their process clearly
- Wants to rewrite everything immediately
- Dismissive of AI-generated code without nuance
- No specific Supabase or database security experience
- Can't provide references from similar work
How long it takes
| Phase | Duration | What happens |
|---|---|---|
| Initial assessment | 1 day | Schema review, high-level code scan, critical issues identified |
| Deep audit | 2 to 3 days | Full code review, security testing, documentation |
| Fix critical issues | 1 to 2 days | Patch immediate security problems |
| Full remediation | 1 to 2 weeks | Complete fixes, testing, deployment prep |
A review and critical fixes take three to five days. Hours vary by scope:
| Scope | Hours |
|---|---|
| Schema and security audit only | 8 to 12 |
| Full code review and documentation | 20 to 30 |
| Review and fix critical issues | 30 to 40 |
| Complete remediation and deployment | 60 to 80 |
If someone quotes far less, they're either inexperienced or planning to cut corners. If they quote far more, they may be padding for a full rebuild.
What you should receive
- An executive summary: the top issues in plain English
- A security report: every vulnerability with a severity rating
- A code quality assessment: patterns, technical debt, maintainability
- A prioritised fix list: what to fix first, second and third
- A deployment checklist: what's needed before and for production
- A recommendation: rebuild or remediate
Rebuild or remediate
Sometimes the answer is to start fresh.
| Remediate when | Rebuild when |
|---|---|
| The core architecture is sound | No consistent patterns exist |
| The issues are fixable in under two weeks | The security issues are fundamental, not just missing pieces |
| The business logic is correct, just insecure | Adding features takes three times longer than it should |
| You're under time pressure and the app is "mostly there" | The developer estimates remediation will cost more than a new build |
A senior developer should give you an honest assessment. If they recommend a rebuild, ask them to explain why in business terms, not just "the code is bad".
Where to start
- Do the five-minute check: open dev tools, find your Supabase key, see what's exposed.
- Write down what you know: features, integrations and any issues you've noticed.
- Find the right person: choose a senior developer with production database, security and AI-code experience.
- Get the audit: three to five days for a thorough review.
- Make the call: remediate or rebuild, based on the findings.
If the call is a rebuild and you'd rather hand it over, that's what Build is: ten business days and £20,000 fixed for one agreed outcome.
The goal is production readiness, not perfection. A good reviewer will help you understand what "good enough" looks like for your stage and get you there efficiently.
Research and sources
Checked 2026-10-18. Next review: 2027-04-18. Listings and availability can change.
API keys | Supabase Docs
Supabase · Checked 2026-10-18 · Primary source
Publishable keys (sb_publishable_) replace the legacy anon key and are meant to be public; secret keys replace the legacy service_role key and bypass Row Level Security.
Row Level Security | Supabase Docs
Supabase · Checked 2026-10-18 · Primary source
A table in an exposed schema without RLS is readable and writable by any role with a grant on it, and auth.uid() returns the ID of the requesting user for use in policies.
Database Functions | Supabase Docs
Supabase · Checked 2026-10-18 · Primary source
A security definer function can return rows the caller isn't allowed to read.
Database Backups | Supabase Docs
Supabase · Checked 2026-10-18 · Primary source
Supabase backs up Pro, Team and Enterprise projects daily, offers Point-in-Time Recovery for more frequent backups, and recommends free projects export their own data.
OWASP Top 10:2025
OWASP Foundation · Checked 2026-10-18 · Primary source
The OWASP Top 10:2025 ranks Broken Access Control as A01, the most serious web application security risk.
Welcome to Lovable | Lovable Documentation
Lovable · Checked 2026-10-18 · Primary source
Lovable is an AI app builder, one of the tools vibe-coded apps are made with.
Bolt AI builder: Websites, apps & prototypes
StackBlitz · Checked 2026-10-18 · Primary source
Bolt is an AI builder for websites, apps and prototypes, one of the tools vibe-coded apps are made with.

