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. Every check below ties to a fixable security pattern you can audit against.
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. When a policy is shipped but returning empty or slow, the Supabase RLS debugging walkthrough covers the seven failure symptoms and the SQL editor role-switching procedure that pinpoints which one.
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. And if your team populates .env.local by hand, share those secrets without committing a .env.
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]. The check also has to fail closed: if getUser() throws instead of returning null, the action must deny rather than fall through to a public path, which the OWASP A10 fail-open patterns catalogue across redirects, catch blocks, and half-finished mutations. The same identity-from-validated-session rule applies to admin-tier MFA operations: the MFA-recovery branch of the auth surface catalogues the 5 lost-device implementation traps including the support-side auth.admin.mfa.deleteFactor() audit-row-before-destructive-call pattern that this check generalizes.
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; the proxy.ts route protection failure-mode catalog covers the matcher, cookie-handler, and redirect-cookie-forwarding traps on the proxy layer itself.
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. For any Route Handler under app/api/, run the same audit against the Route Handler input-validation failure modes: Route Handlers don't get Server Actions' automatic CSRF check, the body can be consumed only once (so the Stripe webhook handler must read request.text() BEFORE any Zod parse), and z.coerce.boolean('false') returns true.
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. For the five rate-limiter failure modes that show up on the Upstash migration (XFF-parsing, fixed-window burst, distributed IPs, missing await, command-billing), the audit gate is the Next.js rate-limit production failure modes catalog: walk each mode against the shipped limiter before launch.
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. Verification and dedup are only the security half; reconciling missed webhook events covers the deliveries Stripe retries for three days and then drops.
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, and raw card data, which is also a PCI-scope problem) belongs in the request body or in headers, not in ?token=.... Anything that still reaches your error tracker should be scrubbed there too: see Sentry data scrubbing for Next.js. 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; the next.config.ts security headers setup guide walks the full 7-header taxonomy and the COOP/Stripe popup trap. 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; for the rare day a critical framework CVE does land, the operational CVE-response playbook covers the 30-minute procedure), 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.
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.
Secure Image Uploads in Next.js: 5 Edge Cases [2026]
Magic-byte checks pass polyglots and miss decompression bombs. Re-encode every image upload through Sharp to strip EXIF and neutralize payloads.