Why Solo Developers Need Security Audits Too (And How to Do It on Zero Budget)
I found 88 potential vulnerabilities in my own code. Here's how I reduced risk by 60% in one day — no consultant, no paid tools.
The Myth: 'My App Is Too Small to Be Attacked'
It's what every solo developer tells themselves. 'I have 50 users, who's going to target my app?' Reality: bots don't filter by size. They scan everything. An open Supabase table, a missing HTTP header, a select('*') on a table with sensitive data — these are open doors.
When I launched Nyura's audit — my productivity app with 90+ Edge Functions — I found 13 critical issues. Not because I'm a bad developer, but because security is the first thing you sacrifice when you ship fast.
1. Defense in Depth: RLS Is Not Enough
Supabase has Row Level Security policies — that's great. But if your client code does .from('tags').select('*') without filtering by user_id, what happens when an admin views another user's data via effectiveUserId? The policy passes, but data gets mixed up.
The fix: add .eq('user_id', effectiveUserId) to EVERY query, even with RLS. That's defense in depth. For join tables like task_tags (no user_id column), use an inner join: .select('task_id, tag_id, tasks!inner(user_id)').eq('tasks.user_id', userId).
2. Are Your HTTP Headers Actually Protecting You?
I had a perfectly configured public/_headers file. Except Nyura deploys on Vercel, not Netlify. Vercel completely ignores that file. My security headers (X-Content-Type-Options, HSTS, X-Frame-Options) were NEVER sent. For months.
Always verify with curl -I https://your-app.com. If you don't see your headers, they don't exist. For Vercel, headers go in vercel.json under headers[].source: "/(.*)".
3. `as any`: TypeScript's Silent Killer
88 as any casts in the codebase. Each one is a place where TypeScript closes its eyes. Worst part: while cleaning them up, I found a real bug. Code was using parent_id instead of parent_task_id in a task merge component. The as any had been hiding the error for weeks.
Reduced from 88 to 38 in one session. Strategy: replace Supabase casts with generated types, add type guards (instanceof Error, 'field' in obj), and only keep legitimate casts (window as any for Capacitor, query interceptor proxy).
4. `select('*')`: The Invisible Waste
65 queries with select('*'). On a tasks table with 24 columns including description (long text), each query transferred far more than needed. On mobile with a 4G connection, it makes a real difference.
Simple rule: select('*') on READ queries → replace with explicit columns. select('*') on WRITE returns (.insert().select('*')) → keep, because the full row updates the React Query cache.
5. Console.log in Production: The Forgotten Info Leak
23 console.log() statements in production. Some displayed RevenueCat tokens, user IDs, and auth events. Anyone opening the browser console could see all of it.
Fix: add "no-console": ["warn", { allow: ["warn", "error"] }] to ESLint. Then convert important operational logs to console.warn and remove the rest. Zero console.log in production.
6. Audit Logging: Your Admin Action Memory
If an admin bans a user, deletes an account, or sends a global notification, who knows? Without an audit log, nobody. We created an audit_log table with RLS (only service_role can write) and a fire-and-forget logAudit() helper that never slows down API responses.
Architecture: the logAudit() function launches the insert in an async IIFE — it returns void immediately, the calling Edge Function never waits. If the insert fails, a console.warn and that's it.
Checklist: Your 30-Minute Audit
1. curl -I https://your-app.com → check security headers
2. grep -r 'select.*\*' src/ → count select('*') queries
3. grep -r 'as any' src/ → count TypeScript casts
4. grep -r 'console.log' src/ → count production logs
5. Verify every client query filters by user_id
6. Verify all routes have Error Boundaries
7. Add audit logging for sensitive admin actions
Total time for Nyura: ~8 hours. Result: 13 critical issues fixed, 50 as any eliminated, 23 logs removed, 17 queries optimized, 15 routes protected with Error Boundaries, and a functional audit table. All without any paid tools.