catnose

Web App Pre-Launch Checklist

Published
Author
Ishikawa

I’ve been keeping a pre-launch checklist for web apps, and it’s grown enough that I thought I’d share it. It’s a mix of lessons from my own mistakes and from things I’ve picked up along the way.

Note
This checklist isn’t exhaustive, it has biases, and some items won’t apply depending on your app’s requirements.

🔒 Authentication cookies

  • HttpOnly is set
    • Helps mitigate XSS, though it’s not a foolproof.
  • SameSite is set appropriately
    • Use Lax or Strict for CSRF defense.
    • If set to Lax (the default in most major browsers), double-check that there are no GET endpoints performing state-changing actions.
  • Secure is set
    • Make sure cookies are only sent over HTTPS.
  • Domain is set deliberately
    • Broader Domain settings send cookies to subdomains, which can pose risks if any subdomain is vulnerable.
    • Prefer host-only cookies: omit Domain to bind the cookie to the exact host (not sent to subdomains).
    • 💡 Tip: Prefix cookie names with __Host- to enforce host-only scope (Secure, Path=/, no Domain, HTTPS only). See MDN.

🔒 User input validation

  • Validation is performed server-side, not just client-side
  • URLs provided by users are validated properly
    • Disallow unsafe protocols (such as javascript:).
    • Make sure regex checks can’t be bypassed (e.g. via case variations, encoding, or partial matches).
  • No raw HTML from users is rendered directly
    • Don’t embed user-provided strings directly with element.innerHTML or React’s dangerouslySetInnerHTML. Sanitize or escape them before rendering.
  • No SQL injection vulnerabilities
  • Handles in URLs are validated properly
    • Example scenario: user URLs look like https://example.com/<handle>
    • Disallow strings that collide with app routes
    • Disallow strings starting with _ (some platforms reserve them, e.g., Google App Engine reserves /_ah)
    • Consider disallowing numeric-only strings (some frameworks treat /404 specially)
    • Maintain a reserved words list (admin, contact, etc) (See: reserved-usernames)

🔒 Response headers

  • Strict-Transport-Security is set

    • Tells the browser to always use HTTPS for your domain, which helps prevent downgrade attacks.
    Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
    
  • CSP frame-ancestors is set

    • Stops other sites from embedding your pages in iframes, a common clickjacking trick.
    Content-Security-Policy: frame-ancestors 'self' # e.g. Allow embed only on your site
    
  • X-Content-Type-Options: nosniff is set

    • Prevents the browser from guessing file types (MIME sniffing), which can cause unexpected behavior. See: MDN.

🔒 Other security checks

  • Recent login is required for especially sensitive actions

    • To mitigate the impact of potential XSS or session hijacking, require re-authentication before critical operations (e.g. account deletion, email change).
  • Dynamic responses aren’t cached by CDN/KV unintentionally

    • Prevents private or user-specific data from being cached and leaked to other users.
  • Public object storage listings are disabled

  • Redirect targets are validated (no open redirects)

    • Prevent https://example.com/login?redirect_to=https://evil.example from sending users to https://evil.example.
  • Authorization checks are enforced for all CRUD operations

    • Only logged-in users with the right permissions can change data.
  • Every DELETE/UPDATE statement includes a safe WHERE clause

    • e.g. you meant to update one user’s products, but ended up updating all products.
      • In my case with Prisma, I disable updateMany/deleteMany except for a few specific WHERE clauses using Extensions.
      • If you’re on Drizzle, eslint-plugin-drizzle is a handy safeguard.
  • User input is not directly included in response headers

    • Prevents header injection and tampering risks.
  • Raw server error messages are not exposed

    • Stack traces and internals stay on the server side.
  • File uploads are validated for type, size, and filename

    • Reduces the risk of malicious uploads or resource exhaustion.
  • Database backups are enabled

  • Object storage backups are enabled

  • 2FA is enabled for all cloud accounts

  • Content Security Policy (CSP) is set up if needed

    • It’s a solid defense. Still, in practice, most projects just don’t have the bandwidth to set it up.

📧 Email Delivery

  • User input in emails can’t be abused for spam
    • e.g. someone puts an ad in their username and it shows up in notification emails.
    • Keep it in check with things like character limits, making user-generated parts clearly marked, or filtering content up front.
  • Mass spamming triggered by user actions is prevented
    • e.g. “follow” action shouldn’t trigger notification emails to thousands of users at once.
    • Cap the number of notifications per user within a given time window to prevent abuse.
  • SPF / DKIM / DMARC are set up
  • Batch emails are deduplicated on retry
    • Many cloud tasks/queues (e.g. AWS, GCP) use "at least once" delivery. Ensure idempotency to avoid duplicate sends.
  • Unsubscribe from newsletters/marketing emails is possible without login
  • Newsletters/marketing emails are configured with List-Unsubscribe=One-Click

📈 SEO

  • A <title> is set on every page
    • Make sure each page has a descriptive and unique title tag.
  • Canonical URLs are set on important pages
    • So that search engines can recognize duplicate URLs as the same content.
  • Error pages are returned with 4xx/5xx
    • Or at least set as noindex.
  • Search result pages are set as noindex or canonicalized
    • Alternatively, clearly indicate in both <title> and <h1> that the page is a search results page (e.g., Search results: [keyword]), so arbitrary query terms don’t get indexed as standalone topics.
    • e.g. if the page title of /search?keyword=BuyVi**ra is simply BuyVi**ra, that term may end up being indexed.
  • Site-wide noindex is not left on by mistake
    • Often left in after staging, intended for removal before release but forgotten.
  • robots.txt is configured
    • Allow/disallow crawlers appropriately. Don't block resources (CSS/JS) Google needs to render the page.
  • Meta descriptions are set on SEO-critical pages
    • Add meta descriptions for pages important for search visibility (e.g., home, product pages). For UGC or less important pages, it’s sometimes better to skip rather than use poor descriptions.
  • An XML sitemap is submitted for public pages
    • Helpful for sites with many public pages. For smaller sites where crawlers can easily follow internal links, it's not necessary.

📱 Open Graph

  • OG tags are set on pages likely to be shared
    • Good to configure og:title, og:description, og:url, og:image, twitter:card (for the card type on X)

💰 Payments

  • Payment provider and app state remain consistent

    • Even if a mismatch occurs, it should be detected and surfaced for review.
    • e.g. a payment succeeds on Stripe but the DB update fails.
  • Duplicate charges are prevented

  • Account deletion does not cause inconsistencies in billing or app logic

  • Subscriptions are auto-canceled on account deletion or freeze

  • Refund/proration rules are clearly stated in the ToS and shown on the cancel page

  • An easy cancel path is provided

  • Receipts/invoices can be downloaded

    • For Stripe, just add a Customer Portal link.
    • Keep it visible even after users cancel. I often run into apps that hide it once you cancel and it’s pretty annoying.

❤️ Accessibility

🚀 Performance

  • JS bundles are kept slim
    • Make sure unnecessary modules aren’t included. Run a bundle analyzer before release to double-check.
  • Static assets are served via CDN
    • Frameworks like Next.js or Nuxt.js generate lots of JS and CSS files. These should be delivered from a CDN rather than the origin.
  • Images are set to avoid layout shifts
    • Use aspect-ratio or width/height attributes on <img> elements.
  • Images are not oversized
    • e.g., avoid loading a 2 MB image that is displayed at only 400px wide.
    • Ideally, use efficient image formats (like WebP or AVIF) and add srcset for proper sizing.
  • Basic SQL indexes are in place
    • Indexes can be added later as data grows, but having essential ones from the start prevents performance issues.

💻 Cross-platform checks

  • The UI displays correctly on phone and tablet sizes

    • I often see this go wrong. It’s worth a quick check.
  • Fonts look natural on all major operating systems

    • If you’re using system fonts instead of web fonts, make sure they look natural on every OS.
  • The layout stays consistent with different scrollbar settings

    • On macOS, Set Show scroll bars to Always and check.
    • Modals can shift the layout when scrollbars are visible. scrollbar-gutter can help.
  • The UI handles long user inputs without breaking the layout

    • e.g. long usernames or long pieces of content should not cause layout issues.

💡 Other Considerations