Skip to main content

Search Defexity

Search pages, guides, free tools and scanner rules

The pre-launch security checklist for AI-built apps

Last updated · 18 min read

Before launching an AI-built app, check fifteen areas, starting with secrets, authentication, database rules, payments and AI cost abuse, then form abuse, uploads, headers, exposed files, dependencies, errors and backups, email, privacy, accessibility and SEO, and re-checking after every deploy. Each item says what to look for and how to prove it is fixed on the live site.

How to use this checklist

AI coding tools are very good at making an app work and much less consistent at making it safe, because "safe" is rarely part of the instruction. Most launch-day problems in AI-built apps are the same dozen mistakes: a secret key in the browser, a database anyone can read, an API route that skips the login check, a webhook that trusts whoever calls it.

Work through the sections in order; the first four cover the problems that cost money or leak data. For each item, the "verify" step is the important part. A change in the code is not a fix until the deployed site behaves differently, so test against the live URL, ideally in a private browser window with no session.

Nothing here needs a security background. Where a step involves code, the sample is short enough to paste into your AI tool with "apply this to my project", and each section links to a more detailed guide or a free check.

Before you start: write down what the app holds

Ten minutes of listing makes the rest of the checklist faster, because it tells you which sections matter most for your app. A landing page with a waitlist and a marketplace that stores payouts need very different amounts of care, and AI tools will not tell you which one you have built.

Keep the list in the repository, for example in a SECURITY.md or your AI tool’s project instructions, so the next change starts from it too.

  1. Data: which personal data you store (emails, names, addresses, messages, files), and where each lives: your database, a storage bucket, a third-party service.
  2. Money: every place money moves or costs accrue, such as payments, payouts, AI calls, email sending, SMS and storage.
  3. Access: who can sign in and the roles they have (visitor, user, team admin, you), and what each role should be able to see and change.
  4. Keys: every API key and secret the app uses, which provider it belongs to, and where it is stored. If you cannot say where a key is stored, start with section 1.
  5. Third parties: every service that receives your users’ data (analytics, error tracking, AI providers, email), so your privacy policy can say so.

1. Secrets and API keys

A secret is anything that lets someone act as your app: payment secret keys, AI provider keys, database passwords, service_role keys, cloud credentials, webhook signing secrets. None of them may reach the browser. Anything in your HTML or JavaScript bundle can be read by every visitor in seconds, and automated scrapers look for exactly these patterns.

The most common leak is a public environment prefix. NEXT_PUBLIC_, VITE_, EXPO_PUBLIC_ and REACT_APP_ variables are copied into the client bundle at build time. They exist for values that are meant to be public, such as a Stripe publishable key or a Supabase anon key, and AI tools reach for them when a server-only variable comes back undefined in the browser.

  1. Search your code and your .env files for public prefixes and make sure each one holds only a public-by-design value.
  2. Move every call that needs a secret key into a server route or server action that reads the key from a server-only variable.
  3. Check that .env files are in .gitignore and were never committed; if one was, rotate every value in it, because git history keeps it.
  4. Rotate any key that has ever been in client code or a public repository. Deleting it from the code does not revoke it.
  5. Verify: open your live site, view the page source and the JavaScript files in the browser’s developer tools, and search for "sk_", "secret", "service_role" and your providers’ key prefixes. Or run the free API key leak checker.

2. Authentication and access control

Authentication answers "who is this?"; authorisation answers "may they see this record?". AI-built apps usually get the first right, because sign-in is a visible feature, and miss the second, because it is invisible. A dashboard page behind a login means nothing if the API route it calls answers anyone who asks.

The pattern to look for is a route that returns data without checking the session, or that checks the session but then loads a record by an ID from the request without checking it belongs to the caller. The second is called an insecure direct object reference: change /api/invoices/41 to /api/invoices/42 and you see someone else’s invoice.

  1. List every API route, server action and serverless function. For each, confirm it checks the session first and returns 401 when there is none.
  2. For each route that loads a record by ID, confirm the query includes the user or organisation (where id = $1 and user_id = $2), not just the ID.
  3. Make admin features check an admin role on the server, not by hiding buttons in the UI.
  4. Confirm password reset and magic links expire, and that sign-up requires email confirmation if accounts hold anything valuable.
  5. Verify: copy an API request from the browser’s network tab and replay it with curl and no cookies. It should return 401. Then sign in as a second test user and request the first user’s record IDs; each should return 404 or 403.

3. Database rules: Supabase and Firebase

Supabase and Firebase let the browser talk to the database directly. That is safe only because of rules that run on the database itself: row level security policies in Supabase, security rules in Firebase. Without them, the public key in your app is enough to read or change every row.

The failure mode is predictable. The first query fails with a permission error during development, and the quickest way to make it work is to turn row level security off, or set the rules to allow everything. That change then ships.

  1. Supabase: in the dashboard, check every table in the public schema has RLS enabled, and that each has policies for the operations the app uses. The Security Advisor lists tables without RLS.
  2. Supabase: make sure the service_role (or secret) key is used only in server code. It bypasses RLS entirely.
  3. Supabase: check views and functions too. A view runs with its owner’s rights unless it is created with security_invoker, which can bypass RLS on the tables underneath.
  4. Firebase: check Realtime Database, Firestore and Storage rules. Remove any ".read": true, ".write": true or allow read, write: if true.
  5. Verify: with only the public key and no session, try to select from each table (Supabase) or open <database>/.json (Firebase). You should get no rows or "Permission denied".
Supabase · row level security for a table of per-user notes
alter table public.notes enable row level security;

create policy "Users read their own notes"
  on public.notes for select
  using ((select auth.uid()) = user_id);

create policy "Users add their own notes"
  on public.notes for insert
  with check ((select auth.uid()) = user_id);

4. Payments and webhooks

Payment bugs cost money directly, in both directions: customers who get the product without paying, and customers who pay and get nothing. Two mistakes cause most of them in AI-built apps.

The first is trusting the browser with the price or the result. If the checkout request includes the amount, someone can change it. If the app unlocks the paid plan when the browser lands on a "success" page, someone can visit that page without paying. The price must come from your server, and access must change only when the payment provider tells your server, through a verified webhook.

The second is an unverified webhook. A webhook endpoint is a public URL; anyone can post a fake "payment succeeded" event to it unless your code checks the provider’s signature on the raw request body.

  1. Create checkout sessions on the server, with prices looked up from your own catalogue or the provider’s price IDs, never from the request.
  2. Grant and remove access only in the webhook handler, after verifying the signature.
  3. Make the handler idempotent: record each event ID and ignore repeats, because providers retry deliveries.
  4. Test the unhappy paths in test mode: a declined card, a cancelled checkout, a refund, a subscription that lapses.
  5. Verify: send a request to the webhook URL without a signature (curl -X POST with any JSON). It must be rejected with 400 and change nothing.
Next.js · Stripe webhook that verifies the signature on the raw body
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: Request) {
  const body = await request.text(); // the raw body, exactly as Stripe signed it
  const signature = request.headers.get('stripe-signature') ?? '';
  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }
  // Look up event.id to skip duplicates, then grant or remove access here.
  return new Response(null, { status: 200 });
}

5. AI features and cost abuse

If your app calls an AI model, every request costs money, and an open endpoint is an invitation for someone to run their own workload on your bill. This is now one of the most common ways small AI products get hurt: not a data breach, but a four-figure invoice.

The protections are boring and effective: the provider key stays on the server, the route requires a signed-in user (or a strict per-IP limit for anonymous features), each request has a token cap, and the provider account has a spend limit so the worst case is bounded.

  1. Confirm the browser never calls the AI provider directly; it calls your route.
  2. Require auth on AI routes, or a tight per-IP and per-day limit if the feature is public.
  3. Cap input length and max output tokens per request, and the number of requests per user per day.
  4. Set a monthly budget or hard limit in the provider’s billing settings, plus an alert below it.
  5. If the app accepts user text and passes it to a model with tools or private data, treat that text as untrusted: the model must not be able to take actions or reveal data the user could not access directly.
  6. Verify: call the AI route with no cookies; it should return 401 or be rate limited after a few requests.

6. Sign-up, login and email abuse

Sign-up, login and password reset are public forms, and bots find them within days of launch. They create fake accounts to use free tiers, try passwords leaked from other sites against your login, and trigger thousands of reset or magic-link emails, which costs money and damages your sending reputation.

None of this needs a clever defence. It needs limits on how often each form can be used, and responses that do not tell an attacker which email addresses have accounts.

  1. Rate limit login, sign-up, password reset and any endpoint that sends an email, per IP address and per account or email address.
  2. Slow down or temporarily lock an account after repeated failed logins, and tell the real owner by email.
  3. Give the same response whether or not an address has an account ("If an account exists, we sent a link"), on both sign-up and reset.
  4. Require email confirmation before granting anything that costs you money, such as free scans, AI credits or storage.
  5. If you see automated sign-ups, add a bot check such as Cloudflare Turnstile or hCaptcha to the sign-up form, verified on the server.
  6. Verify: submit the password reset form twenty times in a row for the same address. After the first few, requests should be refused or delayed, and only a few emails should arrive.

7. File uploads

An upload feature lets strangers put files on your storage. The risks are practical: huge files that run up storage bills, private documents readable by anyone who has the link, and uploaded HTML or SVG files that run scripts when opened from your own domain, where they can read your users’ sessions.

AI tools usually build uploads that work for the happy path: one file type, a public bucket, a URL saved in the database. Each of those defaults is worth a second look before real users arrive.

  1. Limit file size and allowed types on the server, and check the content itself (its signature), not just the file extension or the browser’s claimed type.
  2. Store uploads in object storage, not in the web server’s public folder.
  3. Keep buckets private and hand out short-lived signed URLs for downloads (Supabase Storage policies, S3 presigned URLs), unless the files are meant to be public.
  4. Serve user files from a separate domain, or with Content-Disposition: attachment, so an uploaded HTML or SVG file cannot run as a page on your app’s origin.
  5. Verify: try uploading an .html file renamed to .png, and open the URL of another user’s private file in a private window. Both should be refused.

8. HTTPS and security headers

Hosts such as Vercel, Netlify and Cloudflare Pages handle certificates and HTTPS for you. What they do not add by default on your app’s responses is the set of security headers that tell browsers how to protect your users: Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options and clickjacking protection.

These are configuration, not code, and they take a few minutes per platform. Start the Content-Security-Policy in report-only mode so it cannot break the site, and switch it on once the browser console is clean.

  1. Confirm http:// redirects to https:// on every domain and subdomain you use.
  2. Add Strict-Transport-Security with a max-age of at least 180 days.
  3. Add X-Content-Type-Options: nosniff and frame-ancestors (or X-Frame-Options: DENY).
  4. Add a Content-Security-Policy in report-only mode, fix what it reports, then enforce it.
  5. Verify: run the free security headers checker against the live URL; every row should say "No problem".

9. Exposed files and debug leftovers

Some of the worst leaks are not in the app at all but next to it: a .env file served from the site root, a .git folder that lets anyone download the source and its history, source maps that publish the original code, a phpinfo() page, public API docs listing admin routes. They appear when a deploy publishes the project folder instead of the build output, or when a debugging step is never undone.

  1. Check the publish or output directory in your host settings is the build folder (dist, build, out), not the repository root.
  2. Block dot-folders and dot-files at the server if you run your own, except /.well-known.
  3. Turn off production source maps unless you upload them privately to your error tracker.
  4. Remove debug pages and test routes, and serve API docs only in development or behind sign-in.
  5. Verify: request the common paths on the live site. Each should return 404 (an HTML page with status 200 also means the file is not there, but check the content).
Check the usual suspects from a terminal (replace the domain)
for p in .env .env.local .git/HEAD docker-compose.yml phpinfo.php swagger.json openapi.json; do
  printf '%-20s ' "$p"; curl -s -o /dev/null -w '%{http_code}\n' "https://your-app.example/$p"
done

10. Dependencies and packages

AI coding tools add packages freely, and most of the code in a typical app comes from them. Two problems follow. Outdated packages carry publicly known vulnerabilities that automated scanners look for. And AI tools sometimes suggest package names that do not exist, which attackers can register with malicious code; this is known as slopsquatting, and it works because the install command looks perfectly normal.

  1. Commit the lockfile and install from it in CI (npm ci, or pnpm install --frozen-lockfile), so production runs exactly the versions you tested.
  2. Run npm audit --omit=dev (or pnpm audit --prod) and fix critical and high findings in production dependencies before launch.
  3. Before installing a package an AI tool suggested, check it exists, is the one you meant, and has real downloads, a repository and some history. A brand-new package with almost no downloads deserves suspicion.
  4. Remove packages you no longer use; each one is code you are trusting.
  5. Turn on Dependabot or Renovate so security updates arrive as pull requests instead of being forgotten.
  6. Verify: the audit command reports no critical or high vulnerabilities in production dependencies.

11. Errors, logging and recovery

Launch is when unexpected errors start, and two things decide how bad they get: what the error shows to the visitor, and whether you find out before your users tell you. A stack trace on the screen hands an attacker your file paths, framework version and sometimes query text. Silence means a broken sign-up page can run for days.

Recovery is the other half. AI tools generate migrations and data changes quickly, and one bad migration can destroy data. A backup you have never restored is a hope, not a plan.

  1. Show friendly 404 and 500 pages in production; never raw stack traces or database errors.
  2. Send errors to an error tracker with secrets and personal data scrubbed from the payloads.
  3. Make sure logs never contain passwords, tokens, full card numbers or API keys.
  4. Turn on automatic backups for the database, and do one test restore into a separate project before launch.
  5. Write down how to roll back a bad deploy on your host; most can redeploy the previous build in one click.
  6. Verify: request a URL that does not exist and one that triggers an error (for example a malformed ID); both should show your error page with no internal details.

12. Email deliverability and your domain

Sign-up confirmations, password resets and receipts are part of the product. If they land in spam, users cannot finish signing up and support requests pile up. Since 2024 Gmail and Yahoo require bulk senders to authenticate their mail, and unauthenticated mail from a free mailbox or a new domain is the first to be filtered.

  1. Send from an address on your own domain through a transactional email provider, not from a personal Gmail or Outlook account.
  2. Publish the SPF and DKIM records your provider gives you, and a DMARC record. Start DMARC at p=none with reports, then tighten it once reports look clean.
  3. Keep transactional email (resets, receipts) separate from marketing email, ideally on a subdomain, so a complaint about a newsletter does not block password resets.
  4. Include an unsubscribe link in anything that is not transactional.
  5. Verify: send a reset email to a fresh Gmail and Outlook address. It should arrive in the inbox, and the message headers should show spf=pass, dkim=pass and dmarc=pass.
DNS · a starting DMARC record (TXT on _dmarc.your-domain)
v=DMARC1; p=none; rua=mailto:dmarc-reports@your-domain.example

14. Accessibility and SEO basics

Neither is a security issue, but both decide whether people can use and find what you launch, and AI generators skip the parts you cannot see on screen: the page title, the description, the robots file, alt text and keyboard focus.

  1. Give every page a unique title and description; replace template defaults such as "Vite + React".
  2. Make sure the production homepage is not set to noindex and robots.txt does not block everything (both often leak from preview settings).
  3. Add a sitemap.xml and reference it in robots.txt.
  4. Check text contrast, visible keyboard focus and alt text on meaningful images; every form field needs a label.
  5. Verify: run the free SEO basics checker, and tab through your sign-up form with the keyboard only.

15. After launch: re-check on every deploy

An AI tool asked to add a feature can quietly change something that was already right: remove a header, reopen a database rule, rewrite a webhook without its signature check. The checklist above is not a one-off; the parts that can be checked from outside should run again after every deploy.

Keep the loop short. Scan the deployed site, paste the fix prompt for each finding into your AI tool, deploy, and recheck. Count a problem as fixed only when the live check passes. For the flows that make you money, such as sign-up and checkout, an automatic test after each deploy catches the regressions a scan cannot see.

The whole checklist on one page

AreaCheckHow to verify
SecretsNo secret keys in HTML or JavaScript; no public prefix on secretsSearch the live bundle, or the API key leak checker
SecretsLeaked or committed keys rotatedOld key rejected by the provider
Access controlEvery private route checks the sessionReplay the request with no cookies: 401
Access controlRecords scoped to the ownerSecond test user requests first user’s IDs: 404/403
DatabaseRLS on every table / Firebase rules require authPublic key and no session returns nothing
PaymentsPrice set on the server; access changed only by verified webhooksUnsigned webhook request rejected
AI costsAuth or rate limit on AI routes; spend limit at the providerAnonymous calls rejected or limited
Form abuseRate limits on login, sign-up and reset; no account enumerationTwenty quick resets: later ones refused
UploadsSize and type checked on the server; private files behind signed URLsRenamed .html refused; private URL denied
HTTPS and headersHTTPS redirect, HSTS, CSP, nosniff, frame-ancestorsSecurity headers checker
Exposed filesNo .env, .git, source maps, debug pagesRequest the paths: 404
DependenciesLockfile in CI; no critical or high advisories; suggested packages checkednpm audit --omit=dev is clean
Errors and recoveryNo stack traces; error tracking; tested backupTrigger a 404 and a 500; restore once
EmailOwn domain with SPF, DKIM and DMARCHeaders show spf, dkim and dmarc pass
PrivacyAccurate policy; consent before analyticsNo analytics cookies before consent
SEO and accessibilityTitles, no stray noindex, robots and sitemap, labelsSEO basics checker; keyboard-only sign-up
Every deployRe-scan; test sign-up and checkoutFindings closed only by a passing recheck

Questions

How long does this take?
For a typical small app, an afternoon for the first pass. Most items are configuration or a short change your AI tool can make from a precise prompt; the verify steps take a few minutes each.
Which items matter most if I only have an hour?
Secrets, database rules and access control (sections 1 to 3). Those are where data leaks and surprise bills come from.
Can an automated scan cover all of this?
No. A read-only scan of the live site covers what is visible from outside: exposed keys and files, headers, HTTPS, open database endpoints, SEO basics and more. It cannot log in as your users or read your server code, so access control between users and payment logic still need the tests described above.
Does this apply to Lovable, Bolt, Replit and v0 apps too?
Yes. The tools differ, but the deployed apps fail in the same places. The guides for each tool cover their specific defaults.
I found a serious problem after launch. What do I do first?
Stop the damage before fixing the code: rotate an exposed key, close an open database rule or take the broken route offline. Then fix the cause, check your logs and billing for misuse during the exposure, and decide with advice whether affected users or authorities need to be told. Write down what happened while it is fresh.
Is this legal or compliance advice?
No. Section 13 lists common basics; check the rules that apply to your business and users with a qualified adviser.

Related Defexity checks

Sources

Defexity reports potential risks it can see from outside your app. It is not a guarantee of security and not legal advice.