A pre-launch security audit on a Next.js + Supabase SaaS comes down to 12 highest-impact checks of 30, run in this order: identity (4 checks), input handling (3), payments path (2), perimeter (2), and recovery (1). Together they catch the breach classes documented in real 2026 indie SaaS hacks. The other 18 still matter, but if you only have one afternoon, run these first.
This is the audit, not the launch checklist. An audit is broad and exploratory: you're looking for unknown vulnerabilities across the whole codebase. A launch checklist is narrow and confirmatory: you're verifying specific controls are in place on deploy day [3]. Run the audit weeks before launch, when you still have time to refactor. Run the launch checklist on the morning you flip DNS.
TL;DR:
- The 12 highest-impact checks, grouped by audit phase: 4 identity, 3 input, 2 payments, 2 perimeter, 1 recovery. Each maps to a documented 2026 breach class.
- Run the audit weeks early, not on launch day. The first pass takes two to four hours on a small codebase. Quarterly re-runs take 60 to 90 minutes.
- Triage findings as BLOCK / FIX / ACCEPT. Block anything mapped to a documented breach class. Fix lower-risk gaps in the first sprint after launch. Accept conscious tradeoffs in writing.
- The other 18 checks of the SaaS Security Checklist tool still matter, just not as much for an indie launch. Run the full 30 quarterly once you're live.
- Different from build-time hardening. The Next.js Security Hardening Checklist is the 12 steps you take while writing the code. This is the audit that confirms they all survived.
Table of contents
- When should you run a pre-launch security audit?
- What are the 6 audit phases, and why this order?
- The 12 highest-impact checks, by phase
- How do you triage what the audit finds?
- What about the other 18 of 30?
- How often should you re-run a pre-launch security audit?
When should you run a pre-launch security audit?
Run it two to four weeks before launch, not on launch day. The day before launch is the worst time to discover a vulnerability, because every finding becomes a binary: slip the date or ship the bug. An audit run with time to spare turns each finding into a routine fix, not a crisis.
The deeper reason: an audit and a checklist do different jobs. A pre-launch security audit is broad and exploratory. You're reading code looking for what you don't know is broken. A launch checklist is narrow and confirmatory. You're verifying a fixed list of controls you already built. Both belong, in this order: audit early, fix what you find, then run the launch checklist on deploy day to confirm nothing regressed in the meantime.
If your codebase started as an AI-generated prototype (Lovable, v0, Cursor agent output), the audit takes longer. The vibe-coded migration playbook walks through the additional categories AI-generated code tends to ship broken. For a hand-written codebase, the 12 checks below are usually enough.
What are the 6 audit phases, and why this order?
The phases mirror how an attacker chains exploits, not how features are organized in your repo. An attacker breaks identity first (to act as a user), then attacks input (to get the system to do something it shouldn't), then follows the money, then probes the perimeter, then waits to see if you'll notice. Audit in the same order so each phase you clear closes off what the next phase depends on.
The full set:
| Phase | What it covers | Checks | Why this order |
|---|---|---|---|
| 1. Identity | Auth, sessions, key exposure, IDOR | 4 | Owning identity owns everything else. Audit first. |
| 2. Input | Server Action validation, query construction, rate limiting | 3 | Once identity holds, the next attack surface is what gets sent in. |
| 3. Payments | Webhook auth, financial state changes | 2 | The money path is where attackers monetize the first two failures. |
| 4. Perimeter | Browser-level defenses, transport security | 2 | Headers and HTTPS mitigate the rest, but only after the above hold. |
| 5. Recovery | Logging, incident response | 1 | Detection and response when the first four phases fail anyway. |
| 6. Triage | Decide what blocks launch | n/a | Separate process step; see triage section. |
The order matters because each phase assumes the prior one is solid. There's no point hardening CSP if your SUPABASE_SERVICE_ROLE_KEY is in the client bundle. Fix the foundation first.
The 12 highest-impact checks, by phase
These 12 are drawn from the 30 in the SaaS Security Checklist tool and prioritized against documented 2026 breach data. The Cybersecurity Insight Report's January 2026 scan of 20,052 indie launch URLs found 2,217 domains exposing Supabase credentials in their frontend code, and 2,325 critical exposures involving either a leaked service_role key or tables with no RLS at all [1]. The 170+ Lovable apps disclosed in CVE-2025-48757 fit the same pattern [2]. The 12 checks below close those breach classes.
Phase 1: Identity (4 checks)
Identity failures are the highest-blast-radius bugs in a SaaS. A working session for the wrong user reads every table the real user could read.
Check 1: RLS enabled deny-all on every table. Run this in the Supabase SQL editor:
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
Every row must show rowsecurity = true. Then list policies:
SELECT schemaname, tablename, policyname, roles
FROM pg_policies
WHERE schemaname = 'public';
Zero rows for anon and authenticated roles is the deny-all posture. Your Server Actions still work because service_role bypasses RLS. The browser cannot read anything. This single check would have prevented the bulk of the Lovable breach class [2]. The Supabase docs are explicit that RLS is one defensive layer, not the only one [6], which is exactly why deny-all plus backend-only data access is the safer architectural pair. For the policies you do need, the RLS Policy Generator scaffolds the common safe shapes.
Check 2: No NEXT_PUBLIC prefix on any secret. Run:
grep -rn "NEXT_PUBLIC_.*SECRET\|NEXT_PUBLIC_.*SERVICE_ROLE\|NEXT_PUBLIC_.*PRIVATE" .
grep -E "^NEXT_PUBLIC_" .env.local .env.example
The first command must return zero. The second should only show your Supabase project URL and anon key. Anything prefixed NEXT_PUBLIC_ ships in the JavaScript bundle every visitor downloads. A leaked service_role key is full database admin access for the price of viewing source. Hacktron's CVE-2025-48757 disclosure ran exactly this attack against 170+ Lovable apps [2]. The exposed API keys guide walks through how AI coding assistants reintroduce this mistake when you ask for "quick" client-side queries.
Check 3: Authentication on every Server Action and API route. Server Actions are public HTTP endpoints; anyone who can read your client bundle can call them with any payload. The pattern const user = data.userId from form input never appears in any action that does authorization. Read identity from the session, every time:
'use server'
export async function updateBio(input: unknown) {
const parsed = schema.safeParse(input)
if (!parsed.success) return { error: 'Invalid input' }
// Identity from session, never from input
const user = await getUser()
if (!user) return { error: 'Unauthorized' }
const admin = createAdminClient()
await admin.from('profiles').update(parsed.data).eq('id', user.id)
}
Grep grep -rn "user\.id\|userId" actions/ and verify every hit reads from user (session), not from input or data (request body). This is OWASP A01 Broken Access Control, the #1 category in the OWASP Top 10:2025 [4].
Check 4: OAuth state parameter validation. If your auth flow uses OAuth providers (Google, GitHub, etc.), the state parameter must be validated on the callback. Without it, an attacker can craft a login link that signs the victim into the attacker's account, then collects whatever the victim subsequently submits. Supabase Auth handles this for you when you use the built-in OAuth flow; verify by reading the callback handler and confirming no custom state handling has been bolted on that bypasses the library's check. The OAuth callback secure-implementation guide covers the related next parameter open-redirect pattern and the redirect URL allowlist hardening that Check 4 assumes are in place.
Phase 2: Input handling (3 checks)
Once identity is solid, the next attack surface is what gets sent in.
Check 5: Zod input validation on every Server Action. Grep:
grep -rln "safeParse\|\.parse(" actions/
ls actions/*.ts | wc -l
The two counts must match. Any action file without a safeParse is accepting unvalidated input from any caller. The Authgear analysis ranks "failing to validate inputs on the server" as one of the top three root causes behind 2026 Next.js incidents [3]. For each schema: strings need .min() and .max() bounds, numbers need ranges, enums use z.enum([...]), and objects are explicit about every field. The Server Actions and Zod guide covers the patterns; the JSON-to-Zod converter generates schemas from existing TypeScript types.
Check 6: Parameterized queries only. Never concatenate user input into SQL. With Supabase's .from().eq() builder, this is mostly automatic. The audit is to confirm you haven't introduced raw SQL via .rpc() or .execute() calls that interpolate strings:
grep -rn "supabase.rpc\|\.execute(" .
grep -rn '\$\{[^}]*\}.*FROM\|\$\{[^}]*\}.*WHERE' .
The second pattern catches template-literal SQL, which is the most common modern injection vector. Every hit needs a prepared statement or a Supabase builder call.
Check 7: Rate limiting on auth and sensitive endpoints. Login, password reset, signup, and Server Actions that trigger expensive operations (LLM calls, paid API calls, email sends) need per-IP or per-user limits. Without them, an attacker brute-forces logins, exhausts your email send quota, or drives your AI API bill to four figures overnight. Upstash Redis or Vercel's KV-based rate limiter both ship in 20 lines. The rate limiting Server Actions guide walks through the implementation, including the indie-friendly free tier setup.
Phase 3: Payments path (2 checks)
The money path is where attackers monetize the first two phases. Two checks cover most of the breach class.
Check 8: Stripe webhook signature verification + idempotency dedup. This is the single most common security gap in indie SaaS and it isn't even in the generic 30-check tool, because the tool is stack-agnostic. For Next.js + Stripe, it's the highest-stakes integration in your codebase. Grep:
grep -rn "stripe\.webhooks\.constructEvent" app/api/
The handler must read the raw body, verify the signature with your endpoint secret, and reject anything that fails. Then it must dedupe by event.id against a stripe_events table with a unique constraint. Stripe's docs are explicit: "Use the official libraries from Stripe to verify events" [5]. Without verification, anyone with curl can mark orders as paid. Without idempotency, Stripe's retry behavior turns one purchase into five. The Stripe Webhook Verifier tool tests your signature setup against a known event before you flip the production key, and the 5 causes of Stripe webhook verification failure cover the debugging patterns when it does fail.
Check 9: No sensitive data in URLs or logs. Query strings end up in browser history, server logs, analytics, CDN logs, and proxy caches. Anything secret-shaped (tokens, session IDs, password reset codes, raw email addresses, Stripe customer IDs) belongs in the request body or in headers, not in ?token=.... Audit:
grep -rn "token=\|key=\|secret=" app/ components/ | grep -v "node_modules"
For each hit, the value should be sourced from a cookie or POST body, not from searchParams. The same rule applies to your logger configuration: PII or auth tokens in logs is a slow leak waiting for a log breach.
Phase 4: Perimeter (2 checks)
Browser-level defenses don't prevent breaches on their own, they limit blast radius when something else goes wrong.
Check 10: Security headers configured, including CSP. Your next.config.ts must export a headers() function that sets Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy on every response. CSP uses a nonce for inline scripts, not unsafe-inline. Verify with curl -I https://your-domain.com and confirm all six headers appear. The Next.js docs cover the nonce pattern [7]; the Next.js Security Headers tool generates a copy-paste config with sensible defaults. Missing headers cost nothing to add and block entire categories of attacks at the browser level.
Check 11: HTTPS everywhere with HSTS. Vercel forces HTTPS by default, but you still need Strict-Transport-Security with a long max-age (two years is standard) and preload if you're confident enough to submit to browser preload lists. Without HSTS, a session cookie travels over plain HTTP the next time the user types your domain in a coffee shop's wifi, and an active attacker can downgrade and steal it. This is one line in the headers config; the cost of skipping it is occasional, deterministic session theft.
Phase 5: Recovery (1 check)
When the first 11 checks fail anyway, the question is how fast you detect and respond.
Check 12: Incident response plan + audit logging. You need two things in writing before launch: a one-page incident response plan (who to notify, what to rotate, how to communicate with affected users), and audit logging on at minimum authentication events and admin actions. For Supabase, that's enabling auth event logging in the dashboard and adding a lightweight audit_log table that your Server Actions write to on privileged operations. Without these, your post-incident timeline reads "something went wrong on Tuesday, we noticed Friday." The Sentry setup guide covers the error-aggregation half; the audit log is the separate forensic record of who did what when.
How do you triage what the audit finds?
Every audit produces findings. The decision is what to do with each before launch. Use the BLOCK / FIX / ACCEPT triage, in three buckets:
- BLOCK. Anything that maps to a documented breach class. RLS off on a table with user data.
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEYanywhere in the repo. Stripe webhook handler without signature verification. Server Action that readsuserIdfrom input. Don't launch with any of these. Fix, re-audit the affected check, then proceed. - FIX. Real gaps that aren't yet documented breach vectors for your specific stack but are obvious risks. Missing CSP, no rate limit on a non-auth endpoint, sensitive data in a URL on one route, no HSTS. Ship the fix in the first sprint after launch (week one, not "soon"). Write the ticket the same day you find the gap.
- ACCEPT. Conscious tradeoffs you can defend. No MFA at launch (small audience, low-risk data). Minimal audit logging (no admin actions yet to log). Single-tenant only (B2C, no shared resources). Document the decision in a security notes file, include the date you'll revisit, and move on.
The trap to avoid is the FIX-creep that becomes BLOCK-creep. Every "I'll add CSP next sprint" finding that goes 90 days without action turns into a real risk class. The audit's value is the discipline of writing down what you found AND when you'll fix it, before the launch adrenaline wears off.
What about the other 18 of 30?
The 30-check SaaS Security Checklist covers six categories: Authentication (6 items), Database (5), API (5), Frontend (5), Infrastructure (5), and Monitoring (4). The 12 above cherry-pick the highest-impact items for a Next.js + Supabase indie launch in 2026. The other 18 still matter, just less for the first launch.
Most of them fall into three groups:
- Framework defaults already handle them. Password hashing (Supabase uses bcrypt), encryption at rest (Supabase enables it), automated backups (Supabase point-in-time recovery), secure cookie flags (
@supabase/ssrsets HttpOnly + Secure + SameSite), XSS via raw HTML rendering (React escapes by default unless you explicitly opt into raw HTML injection on a node). - Post-launch monitoring, not pre-launch verification. Failed login alerts, uptime monitoring, MFA rollout. Ship the v1 without them, instrument once you have real traffic.
- Lower-impact for an indie launch. CORS (rarely the vector for SSR-only apps), dependency vulnerability scanning (worth running monthly, rarely the first breach vector), email verification (low impact if your other identity checks hold).
Run all 30 quarterly once you're live. The OWASP Top 10:2025 for Next.js + Supabase maps each category to the specific Next.js + Supabase failure pattern, which is the broader reference if you want to understand the "why" behind every check.
How often should you re-run a pre-launch security audit?
The audit isn't one-and-done. The 12 checks above are also the 12 you re-run quarterly after launch, and the same 12 you run after any significant architecture change (new Server Action category, new third-party integration, new role added to RLS policies). Mark a recurring calendar event the day you flip DNS, set it for 90 days out, and put the audit on your roadmap as standing work.
The fastest re-audit path is to use the SaaS Security Checklist tool interactively: check off each of the 30 items against your current codebase, note any unchecked items in your security log, and triage them with the BLOCK / FIX / ACCEPT framework. The first run takes hours. The fifteenth takes 30 minutes, because by then you've internalized the patterns and you're verifying defaults, not discovering categories.
This is the audit SecureStartKit ships against by default: RLS deny-all is in supabase/schema.sql, every Server Action validates with Zod first, the Stripe webhook handler uses constructEvent with idempotency dedup, security headers are pre-configured in next.config.ts, and no secret is prefixed NEXT_PUBLIC_. The site you're reading runs on it. The audit is what tells you your version of the stack still does too.
Frequently Asked Questions
- What is a pre-launch security audit, and how is it different from a launch checklist?
- A pre-launch security audit is a broad, exploratory review of your codebase weeks before launch, looking for unknown vulnerabilities across the whole application. A launch checklist is a narrow, confirmatory verification on launch day that specific controls are in place. The audit finds what you don't know is broken; the checklist confirms what you already built is still intact at deploy time. You run the audit once, weeks early, then again quarterly. You run the checklist every release.
- Which 12 of the 30 SaaS security checks have the highest breach-prevention impact?
- The 12 highest-impact checks for a Next.js plus Supabase SaaS are: RLS enabled deny-all on every table, no NEXT_PUBLIC prefix on secrets, authentication on every Server Action and API route, OAuth state parameter validation, Zod input validation on every Server Action, parameterized queries only, rate limiting on auth and sensitive endpoints, Stripe webhook signature verification with idempotency, no sensitive data in URLs or logs, security headers including CSP, HTTPS plus HSTS everywhere, and an incident response plan with audit logging. These 12 catch the breach classes documented in 2026 indie SaaS incidents (Lovable, Supaexplorer scan, vibe-coding hacks).
- How long does a pre-launch security audit take?
- Plan two to four hours for the first pass on a small to medium codebase, and a full day if your repo has more than 50 Server Actions or several integrations. The audit is heavier than the 30-minute launch-day checklist because you are reading code carefully and confirming patterns hold across the entire codebase, not just spot-checking. Subsequent quarterly audits run in 60 to 90 minutes because you already know where the surface area lives.
- What do I do with audit findings that I can't fix before launch?
- Apply the BLOCK / FIX / ACCEPT triage. BLOCK findings are anything that maps to a documented breach class (RLS off, NEXT_PUBLIC on a service role key, missing webhook signature verification). These must be fixed before launch, full stop. FIX findings are real but lower-risk gaps (missing CSP, no rate limit on a non-auth endpoint). Ship a fix in the first sprint after launch. ACCEPT findings are conscious tradeoffs (no MFA on launch, minimal audit logging). Document the decision and the date you'll revisit it. Never ship a BLOCK and hope nobody notices.
Built for developers who care about security
SecureStartKit ships with these patterns out of the box.
Backend-only data access, Zod validation on every input, RLS enabled, Stripe webhooks verified. One purchase, lifetime updates.
References
- Vibe Coding Cybersecurity Insight Report (January 2026)— supaexplorer.com
- SupaPwn: Hacking Our Way into Lovable's Office and Helping Secure Supabase— hacktron.ai
- Next.js Security Best Practices: Complete 2026 Guide— authgear.com
- OWASP Top 10:2025 Introduction— owasp.org
- Verify webhook signatures— docs.stripe.com
- Row Level Security— supabase.com
- Content Security Policy— nextjs.org
Related Posts
The Secure SaaS Launch Checklist: 7 Non-Negotiables [2026]
Seven security checks every solo dev must verify before going live: auth, RLS, Zod, webhooks, headers, secrets, error handling. The pre-launch audit.
Vibe Coding Security Checklist: Audit AI Apps [2026]
Vibe coding tools like Cursor and v0 build apps fast, but they often ship vulnerabilities. Here is the technical audit checklist for Next.js and Supabase apps.
Supabase OAuth, Magic Links, MFA in Next.js [2026]
Secure OAuth, magic links, and MFA in Supabase + Next.js. PKCE flow, redirect URL allowlists, AAL2 step-up, and 5 implementation failure modes.