Ship your AI-built app without getting hacked, even if you can't read code.
In the next ~20 minutes you'll know exactly where your app is exposed, fix the obvious holes yourself, and understand what's left. No jargon. Copy-paste where it counts.
Made by Json Jeong, a solo dev in Jeju who shipped his first apps wide open, got burned, and rebuilt. This is Snap to It.
⚠️ One rule before we start: never paste your database keys or admin credentials into a random website to "check" them. Everything in this pack you do yourself, on your own screen.
Part 1 — The 10-Minute Exposure Scan
Go top to bottom. Check the box when you've looked. Don't skip the boring ones, those are where the leaks hide.
☐ 1. Is RLS on for every table with user data? [CRITICAL]
What it is: RLS (Row Level Security) is the lock on your database. Off = anyone with your public key can read or change everything.
Check: Supabase → Database → Advisors → Security. It lists every table with RLS off, and policies that are "Always True" (those are fake-locked, treat as off).
Fix: Turn RLS on for every table holding user data, then add a real policy → see Part 3.
☐ 2. Is your service_role key anywhere in the frontend? [CRITICAL]
What it is: The
service_rolekey is the master key, it ignores all security. It must never reach the browser.Check: Search your code for
service_roleandSERVICE_ROLE. If it appears anywhere outside a server file (e.g.app/api/), that's critical. Then open your live site → F12 (DevTools) → search loaded files for the key.Fix: Move it to server-side only. The browser uses the
anonkey + RLS. Then rotate it → see Part 4.
☐ 3. Did .env get committed to GitHub? [CRITICAL]
What it is: Your
.envholds secret keys. On GitHub (even private), bots find it in minutes.Check: Look for a
.envfile in your repo. Open.gitignore, does it list.env?Fix: Remove it from the repo, add to
.gitignore, and rotate every key that was in it → Part 4. (A key that touched a repo is burned. Deleting it doesn't un-expose it.)
☐ 4. Any secret behind NEXT_PUBLIC_ / VITE_? [HIGH]
What it is: These prefixes ship the value to the browser. Fine for public stuff, fatal for secrets.
Check: Search for
NEXT_PUBLIC_andVITE_. If any is followed bySECRET,KEY,SERVICE, orPASSWORD, that's a leak.Fix: Drop the prefix, move server-side, rotate the key.
☐ 5. Is CORS set to * (wildcard)? [HIGH]
What it is: CORS decides which sites can call your API.
*= any site can, which lets malicious sites use your logged-in users' sessions.Check: Search your API/server code for
Access-Control-Allow-Originset to"*".Fix: Allow only your real domain(s). If your app has login, this isn't optional.
☐ 6. Hardcoded localhost? [breaks in prod]
What it is:
localhost= "this computer." In production it points at the user's machine, which means silent breakage.Check: Search for
localhost.Fix: Use an environment variable so production uses your real domain.
☐ 7. Public storage buckets? [HIGH]
What it is: A public bucket with a broad read policy lets anyone list and download every uploaded file.
Check: Supabase → Storage → check which buckets are Public, and their policies.
Fix: Make user-upload buckets private; serve files through signed URLs or per-user policies.
If you got through these, you're already ahead of ~90% of AI-built apps. Now let's actually fix things.
Part 2 — The Copy-Paste Audit Prompt
Paste this into your AI (Cursor, Claude Code, etc.) with your project open. It scans your code for the issues above:
Audit this codebase for security issues a non-technical founder would miss.
Report ONLY — do not change anything yet.
Check specifically for:
1. Supabase RLS: tables with RLS disabled, and any policy using `USING (true)`,
`WITH CHECK (true)`, or `auth.role()` where ownership (`auth.uid() = user_id`)
was probably intended. List each table.
2. Exposed secrets: API keys, service_role keys, passwords hardcoded in
client-side code or committed files.
3. Any NEXT_PUBLIC_ / VITE_ prefix on a variable whose name implies a secret.
4. service_role key used anywhere outside server-only files.
5. .env files tracked by git.
6. CORS set to "*".
7. Hardcoded localhost / 127.0.0.1.
For each finding: the file + line, why it's dangerous in plain English (assume
I can't read code), and the exact fix. Sort by severity (critical first).
⚠️ What this prompt CANNOT catch (read this)
Whether your RLS rule matches your real intent. It can flag
USING (true), but it can't know ifauth.uid()logic is correct for your app, who should see what is a judgment only you have.Config outside your code. Supabase dashboard settings, email/SMTP, storage rules, not in your repo, so the prompt misses them. (Use Part 1's dashboard checks for these.)
Drift. This is a one-time snapshot. The next time you prompt the AI to add a feature, it can silently re-break a fix. A scan rots in days.
That gap, correct-intent plus always-watching, is exactly the hard part. (More at the end.)
Part 3 — RLS Done Right: Copy-Paste Policy Templates
The #1 mistake: a policy that checks "are you logged in?" (auth.role()) when it should check "is this row yours?" (auth.uid()). The first lets every user read everyone's data.
First, enable RLS on the table (run in Supabase → SQL Editor):
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
Your table needs a user_id column (type uuid) storing the owner. Then pick the case that matches what you want:
Case A — Private data (only the owner can see/touch it)
Use for: personal notes, orders, messages, anything a user shouldn't see from others.
CREATE POLICY "owners read own rows" ON your_table FOR SELECT TO authenticated USING (auth.uid() = user_id);
CREATE POLICY "owners insert own rows" ON your_table FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id);
CREATE POLICY "owners update own rows" ON your_table FOR UPDATE TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
CREATE POLICY "owners delete own rows" ON your_table FOR DELETE TO authenticated USING (auth.uid() = user_id);
Case B — Public read, owner write
Use for: public profiles, published posts, anyone can read, only the owner can change.
CREATE POLICY "anyone can read" ON your_table FOR SELECT TO anon, authenticated USING (true);
CREATE POLICY "owners insert own rows" ON your_table FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id);
CREATE POLICY "owners update own rows" ON your_table FOR UPDATE TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
CREATE POLICY "owners delete own rows" ON your_table FOR DELETE TO authenticated USING (auth.uid() = user_id);
Case C — Admin-only
Use for: internal/admin tables. Tag admins via a custom claim or an admins table.
CREATE POLICY "admins only" ON your_table FOR ALL TO authenticated
USING ( (auth.jwt() ->> 'user_role') = 'admin' )
WITH CHECK ( (auth.jwt() ->> 'user_role') = 'admin' );
(If you don't set custom claims, the safest default is: no client policy at all, and access this table only from server code with the service_role key.)
⚠️ The one to avoid
-- DON'T: this means "anyone logged in sees everything"
USING ( auth.role() = 'authenticated' )
-- (and never USING (true) on private data)
How to verify (do this, it's the real test)
Make two test accounts. Log in as User B and try to load User A's data. If you can see it, your policy is checking "logged in?" not "is it yours?", go back to Case A.
Part 4 — Leaked a Key? The 2-Minute Rotation Guide
A key that was ever exposed (in the browser, a repo, a screenshot) is compromised. Rotate it:
Supabase
service_role/anon: Dashboard → Settings → API → roll/regenerate the key → update it in your server env vars (never the client). (Note: anon is meant to be public, what protects you is RLS, not hiding it.)OpenAI / Anthropic / etc.: provider dashboard → API keys → revoke the old, create new → update server env.
Stripe / payment keys: dashboard → roll keys → update env → re-check webhook secret.
After rotating: remove the old key from any committed file, and make sure
.envis in.gitignore.
OpenAI auto-disables some leaked keys; most providers (including Anthropic) do not, so the usage racked up before you notice is on you. Rotate fast.
Part 5 — The "Safe to Launch?" Gate
Don't go live until all of these are YES:
☐ RLS on for every table with user data, with a real (non-
true, ownership-based) policy☐
service_rolekey nowhere in the browser☐ No secrets in the repo;
.envgitignored; any exposed key rotated☐ Two-test-account check passed (B can't see A's data)
☐ Storage buckets with user data are private
☐ CORS locked to your domain
If any is NO, you're shipping exposed. Fix it first, it's cheaper than the email from a user (or a hacker) later.
What this pack can't do (and what's next)
You can do everything above yourself. What you can't easily do alone:
Be sure every policy matches your real intent as the app grows more complex (3 user types, edge cases…).
Keep it fixed every time you re-prompt the AI, because it silently re-breaks things.
That continuous, intent-aware part is what I'm building SnapDeck for. It watches your app for exactly this, in plain English, on your machine, keys never leaving your computer.
If this helped, subscribe. You'll get the weekly notes (real numbers, what I ship, what breaks) and you'll be first in line for SnapDeck early access. That's the whole deal, no spam.
Run the scan. Make the two test accounts. If you find something scary, good, you found it before someone else did. Hit reply and tell me what you found, I read every one.
— Json 🇰🇷