IS ONLOOK SAFE? SECURITY ANALYSIS | VIBEEVAL
Is Onlook safe? The short answer
Onlook the platform is safe. Apps built with Onlook are safe when someone audits the Next.js layer the visual editor doesn’t show. Onlook produces standard Next.js + TailwindCSS code in a real repository — that’s a better starting position than closed platforms. But the generated apps consistently ship with the same gaps: secrets crossing into the client bundle, route handlers without auth, server actions treated as internal functions, and Supabase tables missing RLS. Each gap is a small fix; collectively they are the difference between a beautiful demo and a production app.
The Onlook generation pattern
Onlook’s core loop is visual: you manipulate the rendered app, and the AI writes the React underneath. The person shipping is often a designer; the code review step often doesn’t exist. That has a predictable cost:
- The boundary blurs. Next.js splits code into server and client. AI edits under “make it work” pressure move fetches into client components or add
NEXT_PUBLIC_to a failing env var — and the secret ships to the browser. - Routes ship without auth. A generated
app/api/orders/route.tsreturns data without checking who is asking. The page in front of it is gated; the JSON is not. - Server actions are trusted as internal. They’re public HTTP endpoints, but generated code treats them like local function calls — no validation, no auth.
- RLS is partial. The first tables get policies; the table added in week three doesn’t. The anon key reads it directly.
- Nobody reads the diff. Visual edits produce code changes that deploy without engineering review — the defining Onlook-shaped risk.
These are not bugs in Onlook; they are characteristics of AI generation plus a visual workflow. Knowing the pattern is the entire fix — every audit item below targets one of those failure modes.
The 6 areas to harden in every Onlook app
1. The server/client component boundary
Anything imported into a "use client" component, and any env var prefixed NEXT_PUBLIC_, is delivered to every visitor. This is by design — the prefix means “public.” The vulnerability is applying it to values that aren’t.
Fix. Keep privileged reads in server components, route handlers, and server actions. Audit every NEXT_PUBLIC_ variable; check the deployed bundle with the Token Leak Checker; rotate anything that ever shipped.
# find env vars that will ship to the browser
grep -rn "NEXT_PUBLIC_" .env* app/ --include="*.ts" --include="*.tsx"
2. Auth on every route handler
Generated route handlers default to “anyone can call this.” The fix is mechanical: resolve the session at the top of every handler that touches user data.
// app/api/projects/route.ts
import { createClient } from "@/lib/supabase/server";
export async function GET() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401 });
const { data } = await supabase
.from("projects")
.select("*")
.eq("user_id", user.id); // ownership scoping, not just auth
return Response.json(data);
}
Note the .eq("user_id", user.id) — auth without ownership scoping is the BOLA pattern that dominates AI-generated CRUD.
3. Server actions as public endpoints
A server action is an HTTP endpoint with a nicer calling convention. Anyone can invoke it with a crafted request, with any arguments.
Fix. Validate and authenticate inside every action:
"use server";
import { z } from "zod";
const UpdateProfile = z.object({
displayName: z.string().min(1).max(80),
// note: no role, no is_admin — allowlist fields explicitly
});
export async function updateProfile(input: unknown) {
const data = UpdateProfile.parse(input);
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("Unauthorized");
await supabase.from("profiles").update(data).eq("id", user.id);
}
The schema allowlist matters: an action that spreads its input into an update accepts { role: "admin" } too.
4. Supabase RLS on every table
The anon key in the browser is safe exactly as long as RLS holds everywhere. Policies must exist per operation — a select-only policy leaves writes open or broken, and a missing policy on one table exposes it entirely.
Fix. Enable RLS on every table, write policies scoped to auth.uid(), and verify from outside with the Supabase RLS Checker:
alter table projects enable row level security;
create policy "own rows" on projects
for select using (auth.uid() = user_id);
create policy "own inserts" on projects
for insert with check (auth.uid() = user_id);
Keep the service-role key server-side only — it bypasses RLS by design.
5. Middleware that doesn’t cover everything
Route protection via middleware.ts is only as good as its matcher. The recurring gap: pages matched, API routes not.
Fix. Treat middleware as defense-in-depth and keep per-handler auth as the real control:
export const config = {
// pages AND api — but handlers still check auth themselves
matcher: ["/dashboard/:path*", "/api/:path*"],
};
A route that fails closed when middleware misses it is fine; a route whose only gate is the matcher is one config edit from open.
6. Build artifacts and review discipline
Two deploy-side checks. First, source maps: confirm productionBrowserSourceMaps stays off, or your original TypeScript ships with the app — see source maps and .git exposed. Second, review: Onlook writes code a designer may never read. Route every deploy through git with a human skim of diffs touching app/api/, actions, middleware, and env usage.
Security headers — the 5-minute win
Generated Next.js apps usually omit security headers. Add them once in next.config:
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" },
{ 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: "/(.*)", headers: securityHeaders }];
},
};
Add a Content-Security-Policy once you’ve inventoried your script sources.
Pre-launch Onlook checklist
A practical sequence to run before pushing an Onlook app to a real audience:
- No server secrets in client components or behind
NEXT_PUBLIC_; deployed bundle checked. - RLS enabled on every Supabase table, policies per operation.
- Every route handler checks auth; every ID-keyed query scopes to the session user.
- Every server action validates input and checks auth/ownership.
- Middleware matcher reviewed; no route relies on it as the only gate.
- Production source maps off; errors generic in production.
- Security headers in place (HSTS, X-Frame-Options, X-Content-Type-Options).
- Rate limiting on auth and any LLM-calling endpoint.
- Two-account BOLA test passed on every user-scoped route.
- Run VibeEval against the deployed URL for the dynamic pass.
Onlook vs Lovable vs v0 — security positioning
- Onlook. Visual editor over real Next.js + Supabase code. Failure modes are the server/client boundary, unauthenticated handlers and actions, RLS gaps, and unreviewed designer-shipped changes.
- Lovable. Opinionated Supabase + frontend generator. Failure modes concentrate in RLS, anon keys, and BOLA.
- v0. UI-first generator. Failure modes follow whatever backend you wire up.
Onlook’s advantage is that everything is inspectable standard code; its risk is that the workflow doesn’t require anyone to inspect it. Pick it when someone on the team will read the diffs.
The verdict
Onlook produces polished Next.js applications quickly, and being open source with standard output is a genuine security advantage. But the visual workflow ships code without forcing a review, and the generated layer consistently leaves out auth, ownership checks, RLS completeness, and boundary discipline. Treat every Onlook-built app as needing a security pass before production — the list is mechanical and scannable. Run it before launch, every launch.
Related resources
- How to Secure Onlook — step-by-step hardening guide
- Onlook Security Checklist — pre-launch checklist
- Vibe Code Scanner — automated scan for Onlook-built apps
- Token Leak Checker — find exposed keys in your deployed bundle
- Supabase RLS Checker — verify RLS from outside
- Vibe Coding Vulnerabilities — full vulnerability taxonomy across AI tools
- OWASP Top 10 for AI Code
Scan your Onlook app
Let VibeEval scan your deployed Onlook application for the boundary leaks, open routes, BOLA, and RLS gaps that visual AI workflows most often leave in.
COMMON QUESTIONS
SCAN YOUR APP
14-day trial. No card. Results in under 60 seconds.