
The Pre-Launch Security Checklist Every Indie Developer Needs
The Pre-Launch Security Checklist Every Indie Developer Needs
You have been building for weeks. The features work. The landing page is ready. You are about to share it on Product Hunt, Hacker News, or Twitter. But have you thought about what happens when strangers start poking at your app?
Most indie developers skip security because it feels overwhelming. Enterprise security guides assume you have a dedicated team, a SOC 2 program, and a six-figure budget. You have none of that. You just need to know: what are the critical things I should check before real users start handing me their data?
This checklist is organized by category with 35 essential checks. Each item tells you what to check, why it matters, and how to fix it quickly. At the end, there is a "minimum viable security" section for when you absolutely need to ship today.
Authentication and Authorization (8 items)
1. Passwords are hashed with bcrypt, scrypt, or argon2
Why it matters: If your database is ever exposed, plaintext or weakly hashed passwords compromise every user account instantly.
Quick fix: Use bcrypt with a cost factor of at least 12. If you are using an auth provider like Clerk, Supabase Auth, or NextAuth.js, this is handled for you.
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12);
2. Login endpoint has rate limiting
Why it matters: Without rate limiting, an attacker can try thousands of password combinations per minute against your login form.
Quick fix: Implement rate limiting at the API level. Limit to 5-10 attempts per IP per 15-minute window. Upstash Ratelimit works well for serverless.
3. Session tokens are cryptographically random and expire
Why it matters: Predictable session tokens can be guessed. Tokens that never expire give attackers unlimited access from a single theft.
Quick fix: Use your framework's built-in session management. Ensure sessions expire after a reasonable period (24 hours for general apps, shorter for sensitive ones). Regenerate session IDs after login.
4. JWT secrets are strong and stored in environment variables
Why it matters: A weak or leaked JWT secret means anyone can forge authentication tokens and impersonate any user.
Quick fix: Generate a random secret of at least 256 bits. Store it in your environment variables, never in code. Explicitly set the algorithm to prevent algorithm confusion attacks.
5. Every API route checks both authentication AND authorization
Why it matters: Authentication confirms who the user is. Authorization confirms they are allowed to do what they are asking. Many apps check the first but not the second, allowing any logged-in user to access any other user's data.
Quick fix: For every database query in an API route, add a WHERE clause that includes the current user's ID:
// Instead of this:
const post = await db.post.findUnique({ where: { id: postId } });
// Do this:
const post = await db.post.findUnique({
where: { id: postId, userId: session.user.id },
});
6. OAuth callback URLs are restricted to your domain
Why it matters: Open redirect vulnerabilities in OAuth flows can be used to steal authorization codes and hijack accounts.
Quick fix: In your OAuth provider's dashboard, set callback URLs to exact matches (not wildcards). Only allow your production domain and localhost for development.
7. Password reset tokens are single-use and expire within 1 hour
Why it matters: Long-lived or reusable reset tokens that leak (via email logs, browser history, or shared links) give persistent account access.
Quick fix: Store reset tokens with an expiration timestamp. Delete the token immediately after successful password change. Hash the token before storing it in the database.
8. Admin routes are protected by role checks, not just authentication
Why it matters: If admin functionality is only protected by a login check, any authenticated user who discovers the admin URL has full access.
Quick fix: Implement role-based access control. Check the user's role from the database (not from the JWT claims) on every admin route.
Data Protection (6 items)
9. Database connection uses TLS/SSL
Why it matters: Without encryption in transit, database credentials and query results can be intercepted on the network.
Quick fix: Ensure your database connection string includes SSL parameters. Most managed databases (Supabase, PlanetScale, Neon) enable this by default. Verify it is not disabled.
10. Sensitive data is encrypted at rest
Why it matters: If an attacker gains access to your database files or backups, encryption at rest prevents them from reading the data directly.
Quick fix: Use a managed database provider that offers encryption at rest (most do by default). For especially sensitive fields like SSNs or payment data, consider application-level encryption.
11. No secrets in client-side code or git history
Why it matters: Secrets in your JavaScript bundle are visible to every user. Secrets in git history persist even after you delete the file.
Quick fix: Search your codebase for hardcoded keys: grep -r "sk_live\|service_role\|secret_key\|password" --include="*.ts" --include="*.js". Check your git history: git log --all -p -S 'secret_key'. If you find leaked secrets, rotate them immediately.
12. File uploads are validated for type and size
Why it matters: Unrestricted file uploads can be used to store malware, exhaust your storage, or execute server-side code if the file is interpreted.
Quick fix: Validate file types using both the MIME type and file extension. Set a maximum file size (10MB is reasonable for most apps). Store uploads in a separate storage service (S3, Cloudflare R2) rather than your application server.
13. User input is never rendered as raw HTML
Why it matters: Rendering user input as HTML enables cross-site scripting (XSS) attacks, where attackers inject scripts that run in other users' browsers to steal sessions or data.
Quick fix: Use your framework's default escaping. In React, JSX escapes by default — never use dangerouslySetInnerHTML with user input. If you need rich text, use a sanitization library like DOMPurify.
14. Database backups exist and have been tested
Why it matters: Without tested backups, any data loss event (accidental deletion, ransomware, corruption) is permanent.
Quick fix: Enable automated backups on your database provider. Download a backup and verify you can restore it to a test environment. Do this before launch, not after an incident.
API Security (6 items)
15. All API endpoints validate and sanitize input
Why it matters: Unvalidated input is the root cause of injection attacks, crashes, and data corruption.
Quick fix: Use a schema validation library like Zod on every API endpoint:
import { z } from 'zod';
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1).max(50000),
published: z.boolean().default(false),
});
export async function POST(request: Request) {
const body = await request.json();
const parsed = createPostSchema.safeParse(body);
if (!parsed.success) {
return Response.json({ error: parsed.error.flatten() }, { status: 400 });
}
// Use parsed.data — guaranteed to match the schema
}
16. API responses do not leak internal data
Why it matters: Exposing database IDs, internal error messages, stack traces, or fields like password hashes gives attackers information to craft targeted attacks.
Quick fix: Use explicit response shaping instead of returning raw database objects:
// Instead of: return Response.json(user);
return Response.json({
id: user.id,
name: user.name,
email: user.email,
// Explicitly exclude: password, internalRole, stripeCustomerId, etc.
});
17. CORS is configured to allow only your domains
Why it matters: Wildcard CORS (Access-Control-Allow-Origin: *) allows any website to make authenticated requests to your API, enabling data theft.
Quick fix: Set CORS to your specific domains. In Next.js, configure this in next.config.js or your API route headers.
18. GraphQL introspection is disabled in production
Why it matters: If you use GraphQL, introspection lets anyone discover your entire API schema, including internal fields and mutations you did not intend to be public.
Quick fix: Disable introspection in your GraphQL server configuration for production environments.
19. API keys and webhooks use signature verification
Why it matters: Without signature verification, anyone who knows your webhook URL can send fake events. Without API key scoping, a leaked key gives full access.
Quick fix: For incoming webhooks (Stripe, GitHub, etc.), always verify the signature header. For API keys you issue, implement scoping and expiration.
20. Large payloads and expensive queries are limited
Why it matters: An attacker can send massive request bodies to exhaust memory or trigger deeply nested database queries to overload your server.
Quick fix: Set express.json({ limit: '1mb' }) or equivalent. For database queries, set reasonable limits on pagination (max 100 items) and nesting depth.
Infrastructure (6 items)
21. HTTPS is enforced on all routes
Why it matters: Without HTTPS, all data including session cookies travels in plaintext and can be intercepted by anyone on the network.
Quick fix: If you are on Vercel, Netlify, or Cloudflare, HTTPS is automatic. Verify that HTTP requests redirect to HTTPS. Check that your cookies have the Secure flag.
22. Security headers are configured
Why it matters: Security headers like Content-Security-Policy, X-Frame-Options, and Strict-Transport-Security protect against XSS, clickjacking, and protocol downgrade attacks.
Quick fix: Add these headers in your framework configuration or hosting provider:
// next.config.js
const securityHeaders = [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
];
23. Environment variables are not committed to git
Why it matters: Anyone with access to your repository (including if it becomes public accidentally) gets every secret in your application.
Quick fix: Add .env* to your .gitignore. Use git log --all -p -- .env to check if env files were ever committed. If they were, rotate every secret in those files.
24. Dependencies are audited for known vulnerabilities
Why it matters: Known vulnerabilities in your dependencies are the lowest-effort attack vector. Automated tools scan for them constantly.
Quick fix: Run npm audit and fix high-severity issues. Set up Dependabot or Renovate for automated dependency updates.
25. Error pages do not leak stack traces or system info
Why it matters: Detailed error messages reveal your tech stack, file paths, database structure, and internal logic to attackers.
Quick fix: Configure a generic error page for production. Log detailed errors server-side only.
26. Unused services and ports are not exposed
Why it matters: Every exposed service is an attack surface. A forgotten Redis instance, database admin panel, or debug port can be exploited.
Quick fix: Audit your infrastructure for exposed ports. Ensure database admin UIs (pgAdmin, Mongo Express) are not publicly accessible. Remove or restrict any development tools from production.
Client-Side Security (5 items)
27. No sensitive data in localStorage or sessionStorage
Why it matters: Any JavaScript on your page (including from third-party scripts and XSS attacks) can read localStorage. Tokens stored there can be stolen.
Quick fix: Use HttpOnly cookies for authentication tokens. If you must use localStorage, never store sensitive data like API keys, personal information, or long-lived tokens.
28. Third-party scripts are audited and minimal
Why it matters: Every third-party script (analytics, chat widgets, ad networks) has full access to your page content and user data. A compromised third-party script compromises your app.
Quick fix: Audit every <script> tag loading external code. Remove any you do not actively need. Use Subresource Integrity (SRI) hashes where possible. Consider a Content Security Policy to restrict script sources.
29. Forms have CSRF protection
Why it matters: Without CSRF tokens, an attacker's website can trick a logged-in user's browser into submitting forms on your application.
Quick fix: Use your framework's built-in CSRF protection. If you use cookie-based auth with a separate frontend, implement the synchronizer token pattern or double-submit cookie pattern.
30. Client-side routing does not expose unauthorized views
Why it matters: Client-side route guards can be bypassed by disabling JavaScript or modifying the client code. If the data loads, it is exposed regardless of the UI.
Quick fix: Authorization must happen server-side. Client-side route guards are a UX convenience, not a security boundary. Always verify permissions in your API routes and server components.
31. Sensitive operations require re-authentication
Why it matters: If a session is hijacked, the attacker can perform critical actions (changing email, deleting account, accessing billing) without proving their identity.
Quick fix: Require password or MFA confirmation for actions like changing email, changing password, deleting account, modifying billing, and generating API keys.
Monitoring and Response (4 items)
32. Application errors are logged and monitored
Why it matters: Without logging, you will not know when attacks are happening or when your application is misbehaving. You cannot respond to what you cannot see.
Quick fix: Set up an error tracking service like Sentry or LogRocket. Configure alerts for error rate spikes and new error types.
33. Failed authentication attempts are logged
Why it matters: Brute force attacks, credential stuffing, and account enumeration all generate failed login attempts. Logging these lets you detect and respond to attacks.
Quick fix: Log failed login attempts with the IP address and timestamp (not the attempted password). Alert on unusual patterns like many failures from one IP or many failures against one account.
34. You have a plan for responding to a security incident
Why it matters: When (not if) something goes wrong, panic leads to poor decisions. Having a basic plan means you can respond quickly and correctly.
Quick fix: Write down: (1) who to notify, (2) how to revoke compromised credentials, (3) how to communicate with affected users, (4) how to preserve evidence. A simple document is infinitely better than no plan.
35. You have a way for people to report security issues
Why it matters: Security researchers who find vulnerabilities in your app need a way to tell you. Without a clear channel, they may disclose publicly or simply not report.
Quick fix: Add a security.txt file at /.well-known/security.txt with your contact email. Even a simple "Email security@yourdomain.com" on your site is better than nothing.
Minimum Viable Security: The Ship-Today Version
If you are launching today and cannot do everything on this list, here are the 10 items that matter most. These cover the vulnerabilities most likely to be exploited in the first week after launch:
- Passwords are hashed (item 1) — a breach without hashing is catastrophic
- Login rate limiting (item 2) — bots will find your login form immediately
- Every route checks authorization (item 5) — IDOR is the most common vulnerability in new apps
- No secrets in client code (item 11) — check your page source right now
- All input is validated (item 15) — use Zod on every endpoint
- CORS is restricted (item 17) — do not leave it as
* - HTTPS is enforced (item 21) — non-negotiable for any app handling user data
- Security headers are set (item 22) — five lines of config that prevent entire attack classes
- Dependencies are audited (item 24) — run
npm auditonce - Errors are logged (item 32) — you need to know when things go wrong
This minimum viable security checklist takes about 2-3 hours to implement. It will not make your app bulletproof, but it will raise the bar high enough that casual attackers and automated scanners move on to easier targets.
Automate This With PreBreach
Going through this checklist manually is valuable, but it is also time-consuming and easy to miss things. PreBreach automates security testing for exactly this scenario: indie developers who need to validate their security posture before launch.
Our 8 AI agents powered by Claude Opus systematically test for every category in this checklist. The 24 custom Nuclei templates are specifically designed for modern stacks — Next.js, Supabase, Firebase, Vercel, Clerk — the tools you are actually using to build.
A PreBreach scan takes 30-60 minutes with plans starting at $29/month. You get a detailed report showing exactly what needs to be fixed, organized by severity, with specific remediation steps.
Ship with confidence. Run your pre-launch security scan now.