AI app security checklist searches often start with a worry about a key visible in browser source. That is worth checking, but for AI-built SaaS products the more consequential question is usually simpler: can the wrong signed-in user read, alter, download, or trigger something that belongs to someone else?

A recent post in r/SaaS brought that gap into focus. The author reported scanning 13 apps made with AI-centric builders including Lovable, Bolt, and v0, finding that the serious issue in the small sample was not a public Supabase browser key but three database tables readable by any authenticated user. It is anecdotal evidence, not an industry-wide prevalence study, but it illustrates a repeatable launch risk: a product can look correct in the UI while its underlying authorization rules are not enforcing tenant boundaries. (reddit.com)

The useful takeaway is not “AI builders are insecure.” AI-assisted development can speed up implementation dramatically, and the same underlying mistakes appear in hand-coded products. The real issue is that a fast build can make authentication, database access, storage, payments, and admin actions feel finished before their security assumptions have been tested. This checklist turns those assumptions into a short, concrete verification routine.

Why AI-built apps need a different launch checklist

AI app builders reduce the distance between an idea and a working application. A founder can prompt for login, a dashboard, subscriptions, file uploads, CRM-style records, and an admin view in a day. That is valuable, but it also compresses decisions that are normally made across frontend, backend, database, and security reviews.

The risk is rarely that a generated interface is visibly broken. The risk is that implementation details behind a correct-looking interface were inferred, stubbed, or connected with incomplete rules. A page that hides another customer’s invoices in React is not secure if the database API would return those invoices to that customer anyway.

Authorization is particularly vulnerable to this illusion because it is invisible when the happy path works. User A signs in, sees User A’s records, edits User A’s profile, and checks out successfully. Unless someone deliberately tests User B against User A’s resources, the product can appear launch-ready.

This is why the r/SaaS discussion resonated. Commenters generally agreed that a two-account check is the highest-leverage pre-launch test, while adding an important caveat: clicking through the interface is only a partial test. The strongest version probes the underlying routes or data API with a second user’s authenticated session, not just the buttons the UI happens to render. That distinction maps directly to OWASP’s guidance on broken object-level authorization: identifiers are not permissions, and every requested object needs an authorization decision. (wstg.owasp.org)

The central distinction: authentication is not authorization

Before running any test, separate two concepts that product teams often bundle together.

Authentication answers, “Who is making this request?” A login system, session cookie, access token, or magic link establishes an identity.

Authorization answers, “May that identity perform this action on this specific resource?” It determines whether that user may read a row, update an invoice, download a file, issue a refund, invite a teammate, or view an internal dashboard.

A generated app can have robust authentication and weak authorization at the same time. Sign-in can work perfectly while every signed-in account can query every customer record. This is the classic shape of a broken access-control issue: the application recognizes a user, but fails to enforce which resources belong to that user or tenant. OWASP notes that these flaws can enable unauthorized viewing, modification, deletion, or execution of functions, and that testing requires multiple accounts and attempts to access content that should remain off-limits. (owasp.org)

The tenant boundary is your real product boundary

For most SaaS apps, the most important security rule is not “users must log in.” It is closer to one of these:

  • A member can access records only within their organization.
  • A customer can access only invoices, uploads, and conversations assigned to their account.
  • A manager can approve expenses but cannot approve their own request.
  • A support employee can view a customer account but cannot export payment details.
  • A system administrator can perform exceptional actions only through a server-side, auditable workflow.

Those rules are business logic. They should exist in database policies, API handlers, server-side checks, and tests—not merely in the client’s navigation menu.

Public keys versus leaked secrets: what browser source really tells you

One of the most common AI app security questions is whether a Supabase key visible in page source means the project has been compromised. The answer depends on the type of key and, critically, on the controls behind it.

Historically, Supabase applications often placed an anon key in the browser. That legacy key identifies the app, rather than granting unrestricted administrative database power, and Supabase’s intended model is for browser clients to use a public-facing key together with Row Level Security (RLS). The platform is transitioning away from the old anon and service_role naming: its current documentation says legacy keys are being deprecated by the end of 2026 in favor of publishable (sb_publishable_...) and secret (sb_secret_...) keys. (supabase.com)

That update matters because the popular “decode the JWT and inspect the role field” advice is useful only for the older JWT-shaped keys, often beginning with eyJ. A modern publishable key is not necessarily a JWT that can be decoded in that way. The security principle has not changed, though: a public client key may be present in a client application; a secret or service-level credential must not be.

What should be visible—and what should never be visible

A browser-delivered JavaScript bundle can be read by anyone. Treat every value embedded in it as public.

Usually expected in a frontend:

  • A public Supabase URL.
  • A Supabase legacy anon key or current publishable key, when the project is designed for browser access.
  • Public analytics identifiers.
  • Stripe publishable keys, typically prefixed pk_.

Urgent to investigate if shipped to the browser:

  • A Supabase legacy service_role key or current sb_secret_... key.
  • A Stripe secret key, typically prefixed sk_live_ in production.
  • Database passwords, cloud-provider credentials, private signing keys, SMTP passwords, or webhook endpoint secrets.
  • A server-only API token that can create users, bypass controls, issue refunds, or retrieve broad account data.

Supabase explicitly states that a client using the legacy service-role credential in its authorization context bypasses RLS. Its current guidance likewise positions secret keys for administrative, server-side operations rather than user-facing access. A secret exposed in production should be removed from the frontend, rotated, revoked where appropriate, and replaced with a server-side integration—not simply hidden in a renamed environment variable. (supabase.com)

The important caveat: public does not mean harmless

A public Supabase key is not an invitation to ignore security. It means the database access layer must be designed under the assumption that an attacker can make requests with that key, inspect network calls, and modify request parameters.

That is exactly why RLS and server-side authorization matter. The public key is safe only in the context of correctly configured grants, policies, and API exposure. Supabase describes RLS as the mechanism that scopes access row by row when a user’s auth token is used with its data APIs. (supabase.com)

The two-account test: the fastest meaningful security check

The core of this AI app security checklist is a two-account test. It is intentionally low-tech because it catches a high-impact category of bugs without requiring a formal penetration-testing engagement.

Create two ordinary accounts in separate tenants or workspaces:

  1. Account A creates identifiable test data: a project, invoice, uploaded file, profile detail, support ticket, message, or subscription-related record.
  2. Account B signs in separately and attempts to locate, read, edit, delete, download, or act on Account A’s test data.
  3. Repeat for every key resource type, including tables that never appear as a prominent screen in the app.
  4. Record the expected result before testing. “B receives a 403 or an empty result” is clear; “B probably should not see it” is not.

Use harmless, obvious canary data such as A_ONLY__DO_NOT_SHARE__2026_09_11 so accidental exposure is unmistakable. Do not use real customer data, real payment records, or production secrets to test your controls.

Test more than reading data

Founders often test only whether User B can see User A’s data. Read exposure is serious, but writes can be worse. A customer who cannot see another customer’s invoice may still be able to guess an ID and mark it paid, change its amount, delete it, or attach it to their account.

For each important resource, test the full CRUD and action surface:

CapabilityQuestion to test as Account B
CreateCan B create a resource inside A’s organization by changing a tenant or owner field?
ReadCan B retrieve A’s record directly, via search, exports, activity feeds, or a guessed URL?
UpdateCan B alter A’s record, ownership, status, or sensitive fields?
DeleteCan B archive or delete A’s resource?
ExecuteCan B resend an email, approve a request, generate a report, issue credit, or trigger an integration for A’s object?

The test should cover both horizontal and vertical access control. Horizontal failures occur between peers, such as one customer accessing another customer’s data. Vertical failures occur when a lower-privilege account reaches an admin-only action. Both can arise from the same shortcut: trusting a client-provided ID, role, organization ID, or UI condition without independently enforcing it on the server or database.

Why clicking through the UI is not enough

The r/SaaS comments made a crucial point: a UI-only test verifies what components fetch and display, not necessarily what the authorization layer permits. If a hidden tab is absent for User B but the underlying endpoint still returns data, the application is vulnerable.

A user with ordinary browser tools can inspect requests in the Network panel, change a URL parameter, replay a request, or call an exposed data endpoint. That is not an exotic attack; it is the normal operating model of a web application where the client receives request details.

A safe direct-request workflow

Test only applications and accounts you own or are authorized to assess. In a staging environment or isolated production test tenant:

  1. Sign in as Account A and create the canary resource.
  2. Capture the normal request used to fetch or update it.
  3. Sign in as Account B in a separate browser profile or private window.
  4. Make the equivalent request with B’s valid session while referencing A’s object ID.
  5. Confirm that the API, database, or server action rejects the request or returns no protected data.
  6. Repeat with mutation routes, file downloads, exports, and bulk actions.

If your stack exposes Supabase’s Data API, test the behavior at that layer as well as in the app. RLS policies are evaluated against the user context and database rows, so a policy that is correct protects requests whether they originate from the intended UI or a direct API call. Conversely, a policy that is missing or too broad cannot be repaired by hiding a button. (supabase.com)

Watch for ownership fields you accidentally trust

Generated code often models a resource with a field such as user_id, owner_id, organization_id, or workspace_id. The field alone does nothing. A secure system must ensure that:

  • The authenticated user is entitled to the target tenant.
  • A create request cannot assign an arbitrary owner or organization.
  • An update cannot transfer ownership unless a deliberately authorized workflow permits it.
  • Queries filter and enforce by server-derived identity, not a client-submitted tenant value.
  • Admin exceptions are explicit, narrow, logged, and tested.

For a simple single-user table, an RLS policy often compares the authenticated identity with the row’s owner identity. For a multi-tenant SaaS, the rule is commonly membership-based: the user must belong to the organization attached to the row. The right policy varies by data model, but “RLS enabled” is not itself a policy design review.

Row Level Security: enabled is not the same as effective

Row Level Security is powerful because it makes the database enforce per-row access rules. But it has to be enabled on exposed tables, paired with the correct privileges and policies, and tested under the roles your users actually have.

Supabase’s current documentation frames securing exposed tables as a combination of grants, RLS policies, and testing. It also cautions that policy behavior and performance need separate attention: RLS should be the security boundary, not merely a convenience filter that the application hopes every query remembers to apply. (supabase.com)

Common RLS mistakes in fast builds

The following patterns deserve review after an AI-generated schema or policy migration:

  • A permissive policy intended for development survives into production. A broad true condition, a policy for authenticated users without an ownership check, or a temporary bypass can make every signed-in user a peer administrator.
  • Only SELECT is protected. The application correctly prevents cross-tenant reads but leaves UPDATE, DELETE, or INSERT policy gaps.
  • The WITH CHECK condition is missing or too broad. A user can create a row assigned to another tenant or change an existing row’s organization_id.
  • Storage is overlooked. Database tables may be protected while file object paths, download URLs, or bucket policies reveal another customer’s uploads.
  • A server route uses an admin credential for user-driven work. A secret key can be appropriate for back-office operations, but it must be paired with explicit server-side authorization before acting on behalf of a user.
  • Sensitive columns remain writable. A user may legitimately update a profile’s display name but should not be able to set their own is_admin, payment status, account balance, or approval state.

Supabase also supports column-level privileges, which can complement RLS when a user should update only selected fields of a row. That is useful when an ownership policy permits an update but not every column should be editable by the owner. (supabase.com)

A practical policy-review question

For every exposed table, say this sentence aloud and make sure the implemented policy proves it:

“Given a request from this exact user, which rows may they select, insert, update, and delete—and why?”

If the answer is “because the frontend only sends their own ID,” the control is in the wrong place. Client requests are inputs, not evidence.

Test routes before login and after logout

Another useful check from the original post is to open sensitive routes in a private window before logging in. The goal is not only to see whether the app redirects to /login; it is to see whether protected data was loaded before that redirect happened.

A client-side redirect is a user-experience control. It may improve navigation, but it is not a security boundary if the browser already received a data payload. This is an inference from the request model: if unauthorized data appears in the HTML, hydration state, API response, or browser network log, redirecting afterward does not make that delivery private.

Review the route in a clean browser session and inspect the Network panel:

  • Does a protected API response return records before authentication is established?
  • Does server-rendered HTML contain a user name, invoice total, email address, or resource ID?
  • Does a preloaded JSON payload include sensitive data?
  • Are cached pages or client-side stores retaining prior-user data after logout?
  • Does navigating directly to an admin route return a proper denial from the server, not merely a visual redirect?

For server-rendered and hybrid frameworks, authorization must occur before sensitive data is fetched or rendered. For client-heavy apps, the data API must reject unauthorized calls regardless of whether the route guard runs.

Files, signed URLs, and exports are part of the attack surface

Database rows get attention because they are visible in schema tools. Files and exports are easier to miss, especially when an AI builder wires upload and download flows quickly.

A SaaS app may protect a documents table correctly yet store file paths in a predictable structure such as org-id/user-id/filename.pdf. If storage rules are broad, a user might request another tenant’s object directly. Likewise, a signed URL should be issued only after the application checks entitlement, and its expiration should match the sensitivity of the file.

Add these checks to the two-account test:

  • Upload a file as A, then try to download it as B through the UI and directly by altering the path or ID.
  • Confirm B cannot list A’s files through search, attachments, previews, exports, or activity logs.
  • Test whether a URL issued to A is still usable by B if shared. This may be acceptable for a deliberately shareable asset, but it should be an intentional product decision.
  • Check previews, thumbnails, OCR output, and metadata endpoints—not just the original object.
  • Test export jobs, CSV downloads, and emailed reports, which often use separate server-side code paths.

The same principle applies to AI features. A conversation, document embedding, generated report, or retrieval result belongs to a user, team, or workspace. If an assistant can retrieve “the right answer” from the wrong tenant’s knowledge base, that is a data leak even if the chat interface itself looks isolated.

Payments and webhooks: do not let the client decide who paid

Authorization failures become especially expensive when they cross into billing. An application should not grant paid access because a browser says checkout succeeded, a client sends an invoice status of paid, or an unverified HTTP request claims to be a Stripe event.

Stripe’s documentation instructs webhook handlers to verify the request signature using the raw request body, the Stripe-Signature header, and the endpoint secret. Parsing or changing the body before verification can break signature validation, which is why framework-specific raw-body handling matters. (docs.stripe.com)

Payment checks to perform before launch

For a product with subscriptions, credits, invoices, or paid feature gates, test these cases:

  1. A user cancels checkout or uses a failed payment method. Confirm no entitlement is granted.
  2. A browser request tries to change plan, price, quantity, customer_id, or payment_status. Confirm the server derives trusted values from your own records and Stripe objects.
  3. A webhook endpoint receives a fabricated request. Confirm it fails signature verification and produces no side effect.
  4. A valid event is delivered more than once. Confirm your handler is idempotent and does not issue duplicate credits, duplicate emails, or repeated account upgrades.
  5. A customer attempts to access another customer’s billing portal session, invoice PDF, or subscription-management route. Confirm server-side ownership checks block it.

Treat webhooks as a privileged integration boundary. They may legitimately update an account, but only after the sending platform is authenticated and the event is connected to the correct internal customer through a trusted mapping.

Turn the two-account test into CI, not launch-day folklore

Manual testing is excellent for discovering what matters. It becomes insufficient once the app changes every week, a reality for products built with rapid prompting and iteration. Each new feature can add a table, route, role, background job, storage bucket, or server action that changes the authorization model.

OWASP recommends automating authorization evaluation because access-control regressions often enter when features are added or modified under time pressure. (cheatsheetseries.owasp.org)

A minimum viable authorization regression suite

You do not need a large security platform to get meaningful coverage. Start with a small test fixture that creates:

  • Tenant A and Tenant B.
  • One standard user in each tenant.
  • One privileged admin or manager where the product has roles.
  • A canary resource, file, message, and billing-related record for Tenant A.

Then write assertions that User B cannot retrieve, mutate, download, approve, export, or trigger actions on A’s resources. Use your application’s normal API layer where possible, then add direct route or data API tests for critical objects.

A useful test matrix looks like this:

ResourceA can readB cannot readB cannot updateB cannot deleteAdmin behavior verified
ProjectsYesYesYesYesYes
FilesYesYesYesYesYes
InvoicesYesYesYesYesYes
Team membersScopedYesYesYesYes
AI conversationsYesYesYesYesYes

Run this suite when database migrations change, permission-related prompts are applied, auth code changes, storage rules change, or payment flows are updated. A test that proves B cannot cross a boundary is more durable than a release checklist item someone may skip.

A 15-minute pre-launch AI app security checklist

If you are launching this week and need the highest-value actions first, use this order. It will not replace a professional review for a high-risk app, but it can expose the most common and damaging configuration errors.

First five minutes: inventory credentials

  • Inspect production client bundles and runtime configuration.
  • Confirm only public client identifiers and intended public keys are delivered to browsers.
  • Search for sk_live, service_role, sb_secret, database URLs with passwords, private keys, and webhook secrets.
  • If a secret appears, remove it from client code and rotate it before proceeding.
  • For legacy Supabase JWT keys, inspect the credential only to identify what it is; do not assume decoding a token proves the app is secure.

Next five minutes: establish tenant isolation

  • Make Accounts A and B in separate workspaces.
  • Create clear canary data as A.
  • As B, search the app, alter object IDs in test URLs, and try the main read and write flows.
  • Inspect network responses for records that the UI does not render.
  • Repeat for files, shared links, exports, and any AI knowledge source.

Final five minutes: check privileged boundaries

  • Visit protected routes while logged out in a private window.
  • Confirm the server or API returns no sensitive data before redirecting.
  • Attempt a payment success or entitlement change without a verified webhook event.
  • Check that an ordinary user cannot call admin actions through a hidden route or modified request.
  • Write down every test that failed, every test that was not possible, and the owner who will resolve it.

This short process is intentionally biased toward impact. It prioritizes cross-tenant data leaks, leaked privileged credentials, and unauthorized payment or admin changes over cosmetic scanner findings.

When a 15-minute checklist is not enough

A basic self-review is a starting point, not a security certification. Move beyond it when the product handles especially sensitive information, significant financial transactions, regulated data, large customer datasets, or business-critical workflows.

You should consider a deeper review when your app includes healthcare, financial, legal, identity, HR, education, payroll, marketplace payouts, enterprise SSO, delegated administration, public APIs, or complex role hierarchies. The same is true when third parties can upload content, trigger automations, access customer data through integrations, or ask an AI system questions over private documents.

A professional application-security review can examine attack paths that are hard to assess from outside the product: authorization logic hidden in server actions, race conditions, webhook replay handling, privilege escalation, SSR data leaks, insecure defaults in cloud configuration, and flawed business workflows such as self-approval. The goal is not to slow down AI-assisted building; it is to apply a level of verification proportional to the harm a failure could cause.

Conclusion: ship fast, but prove the boundaries

The most useful lesson from the r/SaaS scan is not to panic when a public client key appears in source code. Modern frontend applications necessarily expose some configuration to the browser. The more important question is whether every request made with that public information is constrained by effective authorization.

For founders using Lovable, Bolt, v0, or any AI coding workflow, the best pre-launch habit is simple: create two ordinary accounts, seed data in one, and aggressively try to cross the boundary with the other. Then move that test into CI as the product grows.

A polished dashboard is evidence that the happy path works. A passing authorization suite is evidence that the wrong path fails—which is the property customers are actually trusting you to protect.

FAQ

What is the most important AI app security check before launch?

Run a two-account authorization test. Create data as User A, then verify User B cannot read, modify, delete, download, export, or trigger actions on that data through either the UI or direct requests.

Is it unsafe if my Supabase key is visible in browser source?

Not necessarily. Legacy anon keys and current publishable keys are designed for client-side use in the right architecture. However, legacy service-role keys and current secret keys must remain server-side, and public client access is safe only when RLS, grants, and API authorization are correctly configured. (supabase.com)

Does enabling RLS automatically secure a Supabase app?

No. RLS must be paired with correct policies, grants, and tests for each exposed table and action. You need to verify select, insert, update, and delete behavior for users in different tenants. (supabase.com)

Why is a client-side redirect not enough to protect a dashboard?

A redirect controls navigation, not necessarily data delivery. If a browser receives protected API data, server-rendered content, or a preloaded payload before the redirect, the data has already been exposed. Enforce authorization before fetching or returning sensitive resources.

How should I secure Stripe webhooks in an AI-built SaaS app?

Verify Stripe’s signature using the raw request body, the Stripe-Signature header, and the webhook endpoint secret. Do not grant access based on a browser claim that payment succeeded, and make webhook processing idempotent. (docs.stripe.com)