Pre-launch security checklist for AI-built apps
12 checks. The specific gaps Cursor, Lovable, and Bolt apps ship with most often. Supabase RLS, Storage bucket policies, Stripe webhook validation, and secret hygiene. One page. Print it, run through it before you launch.
Database: Row Level Security
RLS is off by default in the SQL editor
Tables created through the SQL editor do not have RLS enabled. Run ALTER TABLE your_table ENABLE ROW LEVEL SECURITY for every table that holds user data. Tables created in the Supabase dashboard Table Editor have it on by default.
Supabase: Row Level Security →RLS enabled with no policies blocks everyone
Enabling RLS without writing any policies denies all reads and writes, including by the row owner. Every protected table needs at least one SELECT policy before your app can read from it.
Supabase: Writing policies →auth.uid() is NULL for anonymous requests
A policy like auth.uid() = user_id silently passes when user_id is also NULL, because NULL = NULL is NULL (not true, not false). Tighten it: auth.uid() IS NOT NULL AND auth.uid() = user_id.
Supabase: Auth helper functions →SECURITY DEFINER functions ignore RLS
A database function marked SECURITY DEFINER runs as the function owner, not the calling user. All RLS policies are bypassed. Every such function needs its own WHERE clause to scope data to the caller.
Supabase: SECURITY DEFINER vs INVOKER →Storage: Bucket Policies
Upsert requires SELECT and UPDATE policies
A Storage policy granting only INSERT silently fails on upsert. Supabase Storage checks SELECT before writing, then needs UPDATE to overwrite. Grant all three or file uploads will appear to hang with no error.
Supabase Storage: Access control →SELECT policy gates listFiles() as well as reads
Listing files in a bucket goes through the SELECT policy, not a separate list policy. If users can read individual files but the SELECT policy is missing, your file browser returns empty with no error thrown.
Supabase Storage: Access control →Missing bucket_id filter lets policies bleed across buckets
A policy without bucket_id = 'your-bucket' in the USING or WITH CHECK clause applies to all buckets in the project. Scope every policy to its specific bucket name.
Supabase Storage: Buckets →owner_id TEXT vs UUID type mismatch
Storing user IDs as TEXT and comparing with auth.uid() (UUID type) produces silent mismatches where rows are never returned or policies never match. Cast explicitly: owner_id::uuid = auth.uid().
Supabase: RLS policies →Webhooks and Payments
Stripe webhook signature not validated
Every Stripe webhook endpoint must call stripe.webhooks.constructEvent() with the raw request body, not the parsed JSON. Passing the parsed body silently breaks signature verification and lets anyone POST fake events.
Stripe: Verify webhook signatures →service_role key used in user-facing API routes
SUPABASE_SERVICE_ROLE_KEY bypasses all RLS. Using it in an API route that accepts user input means a user can trigger queries as the database owner. Reserve it for background jobs and admin scripts running outside request context.
Supabase: API keys and service role →Secrets and Environment Variables
.env file or real secrets committed to git
Run: git log --all --full-history -- .env .env.local .env.production to check. A key in git history stays there even after you delete the file. Rotate any secret that ever touched a commit.
GitHub: Remove sensitive data from a repo →Server-side secrets in NEXT_PUBLIC_ env vars
Any variable prefixed NEXT_PUBLIC_ is bundled into the client JavaScript and visible to anyone who opens DevTools. Only publishable keys belong there. OpenAI API keys, Stripe secret keys, and database connection strings do not.
Next.js: Bundling env vars for the browser →I put this together from the patterns I see repeatedly when doing paid security audits on AI-built apps. If you find something on this list and want a second set of eyes on the fix, that is what the Vibe Audit is for.
Vibe Audit: paid pre-launch security review →matt@uxcontinuum.com
Found something on this list in your codebase? Email me the specific policy or route. Happy to take a quick look at no charge.