Pre-LaunchApril 2, 2026·11 min read

Pre-Launch Security Checklist:
Solo Devs & Indie Hackers

15 items. In priority order. Written for the developer shipping to real users next week — not a Fortune 500 security team. Custodia automates 12 of 15.

The right level of security for a solo launch: not zero, not SOC 2. Cover the 15 items on this list and you have eliminated the vulnerabilities that get real indie products breached. You do not need a penetration test. You need to prevent the obvious attacks before they happen.

The Right Security Standard for a Solo Launch

Most pre-launch security checklists online were written for enterprise teams. They reference WAFs, penetration test scopes, and SIEM platforms that make no sense for a developer who built their app in Cursor and is launching on Product Hunt this Thursday.

The actual risks for a solo SaaS launch are concrete: a hardcoded Stripe key scraped from GitHub, a broken access control bug that lets any user read any other user's data, a missing rate limit that someone runs a brute-force script against, a SQL injection in the search bar. These are not theoretical — they happen to indie products every week, often within days of a Product Hunt launch when the attack surface becomes visible.

This checklist covers exactly those risks. 15 items. Five of them require manual verification. The other ten are things a static analysis scan can check automatically.

15
Total checklist items
11
Scanned automatically by Custodia
4
Require manual verification

The 15-Item Pre-Launch Security Checklist

In priority order. Items marked AUTO are detected by a Custodia scan. Items marked MANUAL require you to verify directly.

01

No credentials in source code or git history

AUTOSecrets

Why: Hardcoded API keys are the fastest path from "public repo" to "compromised account." Bots scan GitHub continuously — a key in a public repo is typically found within minutes.

How to check: Search your codebase for key patterns. Run a secret scan. Check git history with `git log --all -p | grep -E "sk-|aws_|stripe_"`. If you find anything, rotate immediately.

Vulnerable pattern
// ❌ DO NOT commit credentials
const stripe = new Stripe('sk_live_4xK...');
const openai = new OpenAI({ apiKey: 'sk-proj-K9m...' });
Fixed pattern
// ✅ Load from environment at runtime
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
02

.env files listed in .gitignore

AUTOSecrets

Why: Even if you put keys in .env, they get committed if .env isn't ignored. This is the second most common exposure after direct hardcoding.

How to check: Run `git check-ignore -v .env`. If it returns nothing, .env is not ignored. Add it immediately: `echo ".env*" >> .gitignore && echo "!.env.example" >> .gitignore`.

03

Every protected route checks authentication before the handler runs

AUTOAuthentication

Why: AI editors add authentication middleware globally but frequently miss specific route handlers, API endpoints added late, or webhook handlers that assume all callers are trusted services.

How to check: List every file in your `app/api/` directory. For each one: does it call your auth function at the top, before any data access? One missing check = one open endpoint.

// ✅ Check auth at the top of every handler that needs it
export async function GET(req: Request, { params }: { params: { id: string } }) {
  const { userId } = await auth(); // check first
  if (!userId) return Response.json({ error: 'Unauthorized' }, { status: 401 });

  // now do the data work
  const data = await db.query.items.findFirst({ where: eq(items.userId, userId) });
  return Response.json(data);
}
04

Every endpoint checks authorization, not just authentication

AUTOAuthorization

Why: Authentication checks that the user is logged in. Authorization checks that this user is allowed to do this thing. These are different checks. Missing authorization is OWASP A01 — the #1 web vulnerability category.

How to check: Every endpoint that reads or modifies user-specific data based on a URL parameter (like `/api/documents/[id]`) must verify that the authenticated userId owns the resource at that ID. Simply checking that the user is logged in is not enough.

Vulnerable pattern
// ❌ Auth present, authorization absent
export async function DELETE(req: Request, { params }) {
  const { userId } = await auth();
  if (!userId) return new Response('Unauthorized', { status: 401 });
  // ❌ Any authenticated user can delete any document
  await db.delete(documents).where(eq(documents.id, params.id));
  return Response.json({ ok: true });
}
Fixed pattern
// ✅ Auth + authorization
export async function DELETE(req: Request, { params }) {
  const { userId } = await auth();
  if (!userId) return new Response('Unauthorized', { status: 401 });
  // ✅ Verify ownership before mutation
  const doc = await db.query.documents.findFirst({
    where: and(eq(documents.id, params.id), eq(documents.userId, userId))
  });
  if (!doc) return new Response('Forbidden', { status: 403 });
  await db.delete(documents).where(eq(documents.id, params.id));
  return Response.json({ ok: true });
}
05

All database queries use parameterized statements or ORM bindings

AUTOInjection

Why: SQL injection is still one of the most exploited vulnerability classes. It turns a query parameter into admin access or full data dump.

How to check: If you use a query builder or ORM (Drizzle, Prisma, Kysely), you are mostly protected by default — but check for any `db.execute()` or `db.query()` calls with string interpolation.

Vulnerable pattern
// ❌ SQL injection — attacker can modify the query entirely
const result = await db.execute(
  `SELECT * FROM users WHERE email = '${email}'`
);
Fixed pattern
// ✅ Parameterized — input is data, never code
const result = await db.query.users.findFirst({
  where: eq(users.email, email)  // ORM handles escaping
});
06

If you have LLM features — prompt injection prevention

AUTOInjection

Why: If your app builds AI features, user input mixed into system prompts is OWASP LLM01 — the most commonly exploited AI vulnerability. An attacker can override your system prompt with a single message.

How to check: Check every LLM call in your codebase. If any user-controlled variable appears inside the `system` message content (not in the `user` message role), it is vulnerable.

Vulnerable pattern
// ❌ User input in system prompt = prompt injection
messages: [{
  role: 'system',
  content: `You are a helpful assistant. User context: ${userInput}`
}]
Fixed pattern
// ✅ Keep system prompt static, isolate user input
messages: [
  { role: 'system', content: 'You are a helpful assistant.' },
  { role: 'user', content: userInput }
]
07

Auth endpoints rate limited

MANUALRate Limiting

Why: Without rate limits, an attacker can try millions of password/OTP combinations against your login endpoint. Even with bcrypt, 100 concurrent requests can cycle through a dictionary in hours.

How to check: Your auth provider (Clerk, Auth.js, Supabase) may handle this automatically — verify in the settings. If you have custom auth endpoints, add rate limiting middleware at the edge or with a library like `upstash/ratelimit`.

// Rate limiting with Upstash (works on Vercel Edge)
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '60 s'), // 10 req / 60s per IP
});

export async function POST(req: Request) {
  const ip = req.headers.get('x-forwarded-for') ?? '127.0.0.1';
  const { success } = await ratelimit.limit(ip);
  if (!success) return Response.json({ error: 'Too many requests' }, { status: 429 });
  // ... auth logic
}
08

AI/LLM endpoints rate limited to prevent cost spiking

MANUALRate Limiting

Why: An unprotected LLM endpoint can rack up thousands of dollars in API costs within minutes. This is not just a security concern — it is a survival concern for a bootstrapped product.

How to check: Implement per-user rate limits on any endpoint that calls OpenAI, Anthropic, or other pay-per-token APIs. Your Custodia plan already includes scan limits — apply the same thinking to your own product.

09

No known CVEs in your dependencies

MANUALDependency Security

Why: A vulnerable dependency is an attacker-controlled path into your application. npm audit finds known CVEs in your package.json dependency tree.

How to check: Run `npm audit` (or `pip-audit`, `cargo audit`). Fix or audit any High/Critical results. Note: npm audit covers dependencies only — it does not scan your application code.

# Check for dependency vulnerabilities
npm audit

# For Python:
pip install pip-audit && pip-audit

# Fix automatically where possible:
npm audit fix
10

All traffic over HTTPS, HSTS header set

MANUALTransport Security

Why: HTTP connections expose session tokens, credentials, and user data to anyone on the same network. HSTS prevents browsers from ever connecting over HTTP after the first visited.

How to check: If you deploy to Vercel, Netlify, or Cloudflare, HTTPS is enforced automatically. Set the HSTS header: `Strict-Transport-Security: max-age=31536000; includeSubDomains`.

// Next.js — add HSTS to your headers config
// next.config.js
const nextConfig = {
  async headers() {
    return [{
      source: '/(.*)',
      headers: [{
        key: 'Strict-Transport-Security',
        value: 'max-age=31536000; includeSubDomains'
      }]
    }];
  }
};
11

Stack traces and internal errors not exposed to users

AUTOError Handling

Why: Detailed error messages tell attackers exactly which libraries you use, where your code fails, and often the structure of your database schema.

How to check: Catch all errors at the API boundary. Log the full error server-side, but return only a generic message to the client. In Next.js: ensure `NODE_ENV=production` — this disables error overlays and stack traces in responses.

Vulnerable pattern
// ❌ Exposes stack trace, schema, library versions to the client
export async function GET() {
  try {
    const data = await db.query...
  } catch (err) {
    return Response.json({ error: err.message, stack: err.stack }); // ❌
  }
}
Fixed pattern
// ✅ Log internally, return generic message
export async function GET() {
  try {
    const data = await db.query...
  } catch (err) {
    console.error('[API Error]', err); // log server-side
    return Response.json({ error: 'Internal server error' }, { status: 500 });
  }
}
12

Session cookies have Secure, HttpOnly, and SameSite flags

AUTOSession Security

Why: Without HttpOnly, JavaScript can read session cookies — enabling XSS to steal sessions. Without Secure, cookies travel over HTTP. Without SameSite, CSRF becomes possible.

How to check: If you use Clerk or Auth.js, these are set automatically. If you manage sessions manually: `Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax; Path=/`.

13

User input validated and typed at every API boundary

AUTOInput Validation

Why: Unvalidated input is the root cause of injection, logic exploits, and mass assignment vulnerabilities. Parse, never assume.

How to check: Every request body, query parameter, and path parameter should be validated through a schema (Zod is the standard in Next.js/TypeScript). Reject anything that doesn't match.

// ✅ Validate with Zod at the API boundary
import { z } from 'zod';

const CreatePostSchema = z.object({
  title: z.string().min(1).max(200),
  body: z.string().min(1).max(10000),
  // ✅ Only allow fields you expect — prevents mass assignment
});

export async function POST(req: Request) {
  const body = await req.json();
  const parsed = CreatePostSchema.safeParse(body);
  if (!parsed.success) {
    return Response.json({ error: parsed.error.flatten() }, { status: 400 });
  }
  // use parsed.data — only the validated, typed fields
}
14

User-generated content sanitized before rendering as HTML

AUTOOutput Sanitization

Why: Rendering user input as HTML without sanitization is stored XSS — an attacker can inject scripts that run for every future visitor who sees the content.

How to check: If you render user content as HTML (rich text, markdown, or AI output), pipe it through DOMPurify or a server-side sanitizer first. If you don't need HTML, render as plain text.

Vulnerable pattern
// ❌ Stored XSS — attacker injects script into content field
<div dangerouslySetInnerHTML={{ __html: userPost.content }} />
Fixed pattern
// ✅ Sanitize before rendering
import DOMPurify from 'dompurify';
const safeHtml = DOMPurify.sanitize(userPost.content);
<div dangerouslySetInnerHTML={{ __html: safeHtml }} />

// Or: render as plain text if you don't need HTML
<p>{userPost.content}</p>
15

Auth events and critical actions logged

MANUALLogging & Observability

Why: Without logs, you have no way to detect a breach, understand its scope, or prove what happened. Logs are also required if you ever need to demonstrate compliance or investigate a user complaint.

How to check: Log: login successes and failures (with IP), failed authorization attempts, password changes, payment events, admin actions. Store logs somewhere outside your main application (Axiom, Logtail, or even a separate database table).

// Minimal audit log pattern
async function auditLog(event: {
  userId: string;
  action: string;
  ip: string;
  success: boolean;
  metadata?: Record<string, unknown>;
}) {
  await db.insert(auditLogs).values({
    ...event,
    timestamp: new Date(),
  });
}

// Use it on critical paths:
await auditLog({ userId, action: 'login', ip, success: true });
await auditLog({ userId, action: 'delete_account', ip, success: true });
Run the Automated 11-Point Scan

Items 01–06, 08, 11–14 are checked automatically by a Custodia scan. One command covers them all.

# Run in your project root
npx custodia-cli scan

# Takes 3-8 min for a typical Next.js or Node.js project
# Checks: secrets, access control, injection, prompt injection,
#         output handling, error exposure, session security, input validation
  ┌──────────────────────────────────────────────────────┐
  │  CUSTODIA.DEV  //  PRE-LAUNCH SECURITY SCAN         │
  └──────────────────────────────────────────────────────┘

  [SCAN] OpenAI SDK detected → LLM checks enabled
  [SCAN] Scanning 312 files...

  ── CHECKLIST RESULTS ────────────────────────────────

  [✓] No hardcoded credentials found
  [✓] .env excluded from tracked files
  [✗] Authorization missing — CRITICAL
       src/app/api/documents/[id]/route.ts:18
       DELETE handler: auth check present, ownership check absent

  [✗] Prompt injection — CRITICAL
       src/app/api/chat/route.ts:31 — user input in system role

  [✗] Error disclosure — HIGH
       src/app/api/users/route.ts:44 — stack trace in response body

  ─────────────────────────────────────────────────────
  SCORE: 61/100  ·  2 critical  ·  1 high  ·  2 medium
  Fix these before launch → custodia.dev/reports/launch_4mN2p
Run the Automated Scan FreeView Sample Report

What Custodia Covers vs. What You Still Need to Check

Custodia Scans Automatically
  • No credentials in source code or git history
  • .env files listed in .gitignore
  • Every protected route checks authentication before the handler runs
  • Every endpoint checks authorization, not just authentication
  • All database queries use parameterized statements or ORM bindings
  • If you have LLM features — prompt injection prevention
  • Stack traces and internal errors not exposed to users
  • Session cookies have Secure, HttpOnly, and SameSite flags
  • User input validated and typed at every API boundary
  • User-generated content sanitized before rendering as HTML
Check These Manually
  • Auth endpoints rate limited
  • AI/LLM endpoints rate limited to prevent cost spiking
  • No known CVEs in your dependencies
  • All traffic over HTTPS, HSTS header set
  • Auth events and critical actions logged

If Something Goes Wrong Post-Launch

Even with this checklist done, things go wrong. Have these ready before your launch day:

Hosting provider security contact
Vercel: vercel.com/security · AWS: aws.amazon.com/security/ · Cloudflare: cloudflare.com/trust-hub
Payment processor incident line
Stripe: dashboard.stripe.com → Support → Fraud & Security · Contact immediately if payment data was accessed.
Rotate credentials immediately
Know where to go for each: OpenAI, Stripe, AWS, your database provider, your auth provider. Practice this before launch day.
Legal notification obligations
GDPR: 72-hour breach notification to your supervisory authority. CCPA: specific California user notification requirements. Have a lawyer's contact ready.
Preserve all logs
Do not delete anything — logs are evidence. Take a snapshot of your database, server logs, and access logs immediately after detecting an incident.
Key Takeaways
  • You do not need a penetration test for a solo launch. You need these 15 items covered.
  • Broken access control (OWASP A01) is the most common vulnerability in indie apps — check that every endpoint validates both authentication and authorization.
  • Custodia automates 11 of 15 items. Run the scan, fix the findings, then manually check the remaining 4.
  • Have an incident response plan before you launch — know where to rotate every credential, have your hosting provider's security contact ready.
  • After launch, add `custodia scan --diff` to your CI pipeline to catch new vulnerabilities as you ship features.

Frequently Asked Questions

What security checks should I do before launching a web app?

Before launching a web app, check these categories in priority order: (1) Secrets — no API keys or credentials in source code or git history, all secrets in env variables. (2) Authentication — all routes require auth where appropriate, JWTs validated correctly, session management is not broken. (3) Authorization — every API endpoint checks whether the authenticated user has permission to perform the action, not just whether they are logged in. (4) Injection — all database queries use parameterized statements or ORM bindings, never string concatenation. (5) Rate limiting — all auth endpoints have rate limits to prevent brute force. (6) Dependencies — no known CVEs in your npm/pip/cargo dependencies. (7) HTTPS — all traffic over TLS, HSTS header set. (8) Error handling — stack traces and internal errors not exposed to users.

Do I need a security audit before launching as a solo developer?

You need a security scan, not a full security audit. A full security audit (penetration testing, manual code review by a security team) costs $5,000–$50,000+ and is appropriate for Series A+ startups or regulated industries. For a solo developer or indie hacker, an automated static analysis scan covers the vast majority of what matters: OWASP Top 10 vulnerabilities, hardcoded secrets, broken access control, injection flaws, and AI/LLM security if you have those features. Run `npx custodia-cli scan` — it takes 5 minutes and costs nothing on the free tier.

What is the most common security mistake solo developers make?

The most common mistake is missing authorization checks — specifically, checking that a user is authenticated but not checking whether they are authorized to access the specific resource. This is OWASP A01 (Broken Access Control) and it consistently ranks as the #1 web application vulnerability. The pattern looks like: the developer adds `if (!userId) return 401`, which blocks unauthenticated requests, but forgets to add `if (userId !== resource.ownerId) return 403`, which would block authenticated-but-unauthorized requests. Every API endpoint that reads or modifies user-specific data needs both checks.

How do I check if my dependencies have known vulnerabilities?

For Node.js: run `npm audit` to check for known CVEs in your package.json dependencies. For Python: run `pip-audit` or `safety check`. For Rust: run `cargo audit`. Note that these tools only cover dependencies — they do not scan your application code for vulnerabilities you introduced. For application-level vulnerabilities (injection, broken access control, secrets), you need a SAST tool like Custodia. Use both together: `npm audit` for dependency CVEs, `npx custodia-cli scan` for application logic vulnerabilities.

What happens if my app is breached right after launch? Who do I contact?

Immediate steps after a breach: (1) Take the affected service offline or restrict access if you can do it quickly. (2) Rotate all API keys and credentials that may have been exposed. (3) Contact your hosting provider (Vercel, AWS, etc.) — they have security incident teams and may be legally required to assist. (4) If you collect user data, you likely have legal obligations to notify affected users within 72 hours under GDPR or equivalent laws — consult a lawyer. (5) Preserve logs — do not delete anything that might help you understand what happened. (6) If payment data was involved, contact your payment processor (Stripe, etc.) immediately — they have PCI DSS obligation handling.

Related Articles

API Key Exposed in Your Code?
Emergency 5-step response for leaked credentials — rotate, audit, scan, block, prevent.
GitHub Actions Security Scanner
Add OWASP Top 10 scanning to your CI pipeline. Complete YAML template, no config needed.
OWASP Top 10 Code Review Guide
How to find SQL injection, broken access control, and XSS in your codebase — with real code examples.
Launch Clean

Run the
Automated 11-Point Scan

Free — 3 scan credits. OWASP Top 10 + OWASP LLM Top 10 + secrets. Results in 5 minutes. See pricing →

Get My Free API KeyView Demo Report →