How to security-check an AI-built Next.js app before launch
Use observable launch gates for secrets, authentication, authorization, data access, billing, and production configuration before exposing real users or money.
Contents11 sections
- What you need before testing
- Gate 1: server secrets never reach the browser
- Gate 2: protected content is rejected before it renders
- Gate 3: the wrong signed-in user cannot cross the data boundary
- Gate 4: every mutation rebuilds authority on the server
- Gate 5: payment state becomes application access through a verified server path
- Gate 6: prove the exact production configuration you will expose
- Keep a small launch record
- What this does not prove
- What CoreMVP already resolves in the common foundation
- What to do next
An AI-built app does not need a separate definition of security. It needs the same trust boundaries as any other application, proved against the application that will actually handle users, data, and money.
The practical difference is speed. AI can give you working code before you have a complete mental model of what the code trusts. Before launch, turn that uncertainty into a small set of observable gates.
A gate is not passed because the code looks reasonable or the happy path works. Pass it only when the intended path succeeds and a realistic unauthorized or invalid path fails.
That is the standard worth holding any startup foundation to: the important boundary should be both usable and difficult to misuse.
This is a bounded pre-launch check, not a security certification. It does not replace a threat model, dependency review, penetration test, OWASP ASVS assessment, privacy review, or specialist review for sensitive systems.
What you need before testing
Use a production-like environment, or the exact production deployment when a provider or callback boundary is part of the check.
Prepare:
- two test users, A and B;
- at least one private record owned by each user when your product stores user-owned data;
- access to the deployment, Supabase, and Stripe configuration you actually intend to use;
- Stripe test mode when the product charges money;
- browser developer tools or another way to inspect requests and responses.
Do not use real customer data for these checks.
The six gates below are intentionally narrower than a generic security checklist. They target the places where a working application can create false confidence.
Gate 1: server secrets never reach the browser
Next.js makes the browser boundary explicit: values prefixed with NEXT_PUBLIC_ can be included in client-side JavaScript. Values without that prefix can stay server-only when you keep their readers on the server. See the current Next.js environment-variable guidance.
Classify every credential before launch.
| Credential type | Expected boundary |
|---|---|
| Supabase publishable key | Browser-safe configuration |
| Supabase secret key | Server only |
| Stripe publishable key | Browser-safe configuration |
| Stripe secret key | Server only |
| Stripe webhook signing secret | Server only |
| Direct database URL | Server only |
Supabase makes the same distinction between publishable keys and server-side secret keys. A secret key has elevated authority and must not be treated like an ordinary user credential. Supabase API-key guidance documents that boundary.
Positive proof: the deployed browser can perform the public operations it needs with public configuration.
Reject proof: inspect the browser bundle, storage, rendered HTML, and relevant network traffic. No server-only credential should appear there.
If a server secret reached the browser, rotate it after removing the exposure. Deleting the string from source does not revoke a credential that has already been delivered.
Gate 2: protected content is rejected before it renders
A login screen is not proof that private routes are protected.
Next.js treats authentication, session management, and authorization as separate concerns. Its current authentication guidance also warns against relying only on UI-level restrictions. Route Handlers, Server Actions, data reads, and other server entry points still need checks at the boundary they protect.
Pick one route that should require a signed-in user.
Test both paths:
- Sign in as a valid test user and confirm the private route works.
- Open the same route without a valid session, including by requesting the URL directly.
The second request must not receive private content before a client-side redirect runs. A redirect in useEffect, a hidden button, or a disabled navigation item is user interface behavior, not server admission.
Positive proof: the authenticated request reaches the intended private reader.
Reject proof: the anonymous request is redirected, denied, or receives a non-sensitive response before private content is returned.
Repeat this at any other server entry point that exposes the same protected capability. A protected page does not automatically protect its API route or Server Action.
Gate 3: the wrong signed-in user cannot cross the data boundary
Authentication answers who the user is. It does not prove that the user may read or change a particular record.
Create one private record for user A and another for user B. Then test the application as user A.
First prove the normal path: user A can read or update A's record.
Then change the target to B's record. Use the same application path a real caller can reach: a changed URL, request body, query parameter, or direct API request.
The request must be denied or behave as not found. It must not return or mutate B's record.
The enforcement point depends on your architecture:
- If browser code accesses Supabase's Data API directly, database grants and Row Level Security must enforce the user boundary.
- If your server uses a privileged database connection, your server authorization and repository/query scope must carry the user or tenant boundary into the read or write.
Do not accept “RLS is enabled” as proof when the failing path uses a server credential that bypasses that policy. Do not accept “the API checked membership” as proof when the following database query can still load another tenant's record by ID alone.
Positive proof: A can access A's resource.
Reject proof: A cannot read or mutate B's resource through the same public capability.
This negative case catches a class of bugs that happy-path tests never exercise.
Gate 4: every mutation rebuilds authority on the server
Anything the browser can send is input, even when your own UI generated it.
That includes:
userId;ownerId;organizationId;role;- resource IDs;
- redirect destinations;
- price or plan identifiers;
- feature flags sent by the client.
For one sensitive mutation, intercept or reproduce the request and change a value that would grant more authority if the server trusted it.
The server should validate the input and rebuild the important facts from trusted state: the current session, persisted ownership or membership, and server-owned configuration.
A useful test is deliberately boring. Change ownerId to another user. Change an organization ID. Submit a resource ID that belongs to someone else. Try a price ID your application does not sell.
Positive proof: a valid request changes the intended resource.
Reject proof: the tampered request is rejected before the protected side effect occurs.
Do not stop at schema validation. A syntactically valid UUID can still identify a resource the caller does not own.
Gate 5: payment state becomes application access through a verified server path
If your app charges money, do not treat the browser's Checkout success page as payment proof.
Stripe recommends verifying webhook signatures against the raw request body. See Stripe webhook signature verification.
The useful application path is:
Test this in Stripe test mode.
Prove that a successful test payment causes the signed webhook path to write the durable state your protected reader uses. Then test the false-success cases your implementation claims to handle.
At minimum:
- loading or guessing a success URL must not grant access by itself;
- an invalid webhook signature must not mutate billing state;
- a repeated event must not create duplicate access or duplicate side effects when your application claims idempotent processing.
Positive proof: the verified provider event results in the expected persisted access and the protected reader observes it.
Reject proof: browser navigation or an unverified event cannot create that authority.
A green payment UI is not enough if the product later authorizes from different state.
Gate 6: prove the exact production configuration you will expose
Local proof cannot verify a hosted callback URL, a production environment variable, a Stripe webhook endpoint, or which Supabase project your deployment is connected to.
Before launch, verify the exact deployed target.
Check that:
- the deployment points to the intended provider projects and accounts;
- production callback and redirect URLs match the deployed domain;
- server secrets exist only in server-owned production configuration;
- Stripe webhooks target the intended production route and mode;
- schema and migrations are present in the database the application actually reads;
- the deployed protected route still rejects an anonymous request;
- the deployed cross-user or cross-tenant negative case still fails;
- the deployed billing path works in the mode you intend to launch.
Then trigger at least one safe expected failure and inspect what the application exposes. Errors and logs should help you find the failing boundary without returning passwords, session tokens, database URLs, webhook secrets, or payment details to the user.
Positive proof: the real deployment completes the intended path using the intended provider state.
Reject proof: the same deployment rejects an invalid or unauthorized request without leaking sensitive material.
Preview and local environments are useful earlier. They are not substitutes for proving configuration that only exists in production.
Keep a small launch record
Keep the result small enough that you can rerun it after a material change.
| Gate | Positive evidence | Negative evidence |
|---|---|---|
| Secrets | Public client works with public configuration | No server secret is delivered to the browser |
| Authentication | Signed-in user reaches private route | Anonymous direct request cannot receive private content |
| Data authorization | User A reaches A's record | User A cannot read or mutate B's record |
| Mutations | Valid input performs the intended action | Tampered authority/resource input is rejected |
| Billing | Verified event creates durable access | Success URL or unverified event cannot grant access |
| Production | Exact deployment completes the real path | Exact deployment rejects one expected bad path safely |
If you cannot produce the negative evidence for a gate, treat that gate as unproved. Reading the code again is useful, but it is not the same evidence.
What this does not prove
Passing these gates does not prove that the application is secure.
It does not cover every vulnerability class, dependency issue, denial-of-service risk, abuse path, cryptographic problem, privacy obligation, or product-specific threat. It also does not establish compliance with OWASP ASVS or any legal or regulatory standard.
Add deeper review when the consequence of failure is higher. Applications handling regulated data, high-value financial actions, sensitive identity data, complex multi-tenant permissions, untrusted file processing, or security-critical infrastructure need a broader threat model and specialist testing.
The point of these gates is narrower: remove obvious false confidence before you expose a real application.
What CoreMVP already resolves in the common foundation
These six gates are not exotic enterprise controls. They are the common boundaries a real startup eventually has to get right.
CoreMVP is built so the common parts of those boundaries do not become six new infrastructure projects after your prototype starts working.
The shipped foundation already gives you:
- Supabase Auth with server-owned session handling and protected route admission;
- application data through Supabase Postgres, migrations, Drizzle, and explicit server-owned repositories;
- Stripe Checkout whose verified webhook path writes durable billing and access state;
- explicit public-versus-server environment boundaries;
- one supported managed deployment path on Vercel;
- maintained proof paths for authentication, billing, database connectivity, and the hosted application.
That changes what your launch review is for.
Instead of spending the review inventing the common auth, billing, secret, database, and deployment boundaries from scratch, you can spend it on the risks created by your product: its resources, permissions, tenant model, uploads, AI/tool integrations, providers you add, and the actions unique to your users.
That is the value of a premium foundation. It does not promise that your finished application is automatically secure. It removes a large amount of common security-sensitive work before your product-specific risk begins.
For the exact shipped paths, continue to Authentication, Database, Billing, and Deployment.
What to do next
If a gate above fails, fix that boundary before adding more launch checklist items.
If you are building the foundation yourself, use How to add authentication to Next.js and How to add Stripe billing to Next.js for two common implementation paths.
If you are building on CoreMVP, start the security review where the foundation stops: the product-specific behavior only your startup introduces. Continue to the CoreMVP Docs to inspect the supported paths and their proof.