← Blog Nyura Blog
Cybersecurity and data protection — luminous network

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.

March 5, 2026 3 min read Cyril Simonnet
SecurityIndie DevTypeScriptSupabaseGEO
Neon banana on purple background — bugs hide everywhere

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.

Colorful protection layers — like a funky onion

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).

Neon shield in a digital tunnel

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: "/(.*)".

Green Matrix code — errors hide in the type system

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).

Colorful bananas fanned out — only take what you need

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.

Golden padlock on pastel background — lock down your logs

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.

Open journal with golden pen — every action matters

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.

Colorful checklist on modern desk

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.

Try Nyura for free

Available on iOS, Android, and web. No credit card required.

Get Started →