7 Security Checks Before Shipping an AI-Built Next.js + Supabase App
AI coding assistants can dramatically reduce the time between an idea and a working application. Unfortunately, they do not reduce the application’s attack surface.
Generated code often looks reasonable, compiles successfully, and passes the happy-path test. The dangerous mistakes tend to live in the assumptions around that code: who is allowed to call an endpoint, which credentials are exposed, and whether one user can access another user’s data.
Before shipping an AI-assisted Next.js and Supabase application, I check these seven areas.
1. Search for exposed secrets
Start by checking the repository and client bundle for credentials.
In Next.js, any variable prefixed with NEXT_PUBLIC_ can be included in browser-delivered code. A Supabase publishable or anonymous key is intended for client use, but a service-role key is not.
Look for:
- Service-role keys in frontend code
- API keys committed to the repository
- Secrets printed in build or application logs
- Environment files accidentally tracked by Git
- Server-only modules imported by client components
Rotating a secret after exposure is safer than simply removing it from the latest commit. Git history, build artifacts, logs, and deployment previews may still contain the original value.
2. Verify authorization on the server
Hiding a button is not authorization.
Every sensitive route, server action, and API handler should verify the caller’s identity and permissions on the server. Never trust a user ID, organization ID, role, or ownership field merely because the frontend supplied it.
A dangerous pattern looks like this:
const { userId } = await request.json();
const { data } = await supabase
.from("documents")
.select("*")
.eq("user_id", userId);
The caller controls userId. Instead, derive identity from a verified session or token and use that verified identity in the query.
3. Test Row Level Security adversarially
Enabling Row Level Security is only the beginning.
For every user-owned or tenant-owned table, test at least four cases:
- An authenticated user can access their own row.
- That user cannot access another user’s row.
- An unauthenticated request is rejected.
- Inserts and updates cannot assign ownership to someone else.
Remember that SELECT, INSERT, UPDATE, and DELETE may require separate policies. For writes, verify both which existing rows can be targeted and which new row values are allowed.
The most valuable RLS test is often not “Can Alice read Alice’s data?” It is “Can Alice read or modify Bob’s data?”
4. Validate input at every boundary
TypeScript types disappear at runtime.
Validate request bodies, query parameters, webhook payloads, uploaded-file metadata, and structured AI output before using them. A schema validator such as Zod can help, but the important part is treating every external value as untrusted.
Validation should cover more than shape. Also enforce:
- Maximum lengths and collection sizes
- Allowed enum values
- Numeric and date ranges
- File type and size restrictions
- Unknown-field handling
- Business rules such as ownership and valid state transitions
5. Add abuse controls
An authenticated endpoint can still be abused.
Apply rate limits to expensive or sensitive operations such as AI generation, authentication attempts, email delivery, file processing, and public forms. Set request-size limits and timeouts as well.
For browser-facing APIs, configure CORS intentionally. Avoid combining a wildcard origin with credentials, and do not treat CORS as an authorization mechanism—it only controls participating browsers.
6. Secure dependencies and CI/CD
A clean application scan does not guarantee a safe delivery pipeline.
Check that:
- Lockfiles are committed and used in CI
- Third-party CI actions are pinned
- Workflow permissions follow least privilege
- Untrusted pull requests cannot access deployment secrets
- Security jobs cannot silently fail
- Dependency installation is isolated from publishing and production credentials
- Container images run as a non-root user where practical
Build jobs execute code from your dependencies. Treat them as part of the application’s security boundary.
7. Review AI-agent permissions and instructions
Agent configuration is executable policy written in natural language.
Review repository instructions, tool permissions, hooks, MCP servers, and automated commands with the same care as source code. An agent should not receive production credentials or destructive permissions simply because those permissions make development more convenient.
Prefer:
- Least-privilege tool access
- Explicit approval for destructive or external actions
- Sandboxed execution
- Separate development and production credentials
- Logs tied to the exact commit and command
- Human review before deployment
A compact pre-launch checklist
Before shipping, confirm that:
- No privileged secret reaches the client or repository
- Every sensitive server operation verifies identity and authorization
- Cross-user and cross-tenant access tests fail safely
- External input is validated at runtime
- Expensive endpoints have abuse controls
- CI jobs use minimal permissions and isolated secrets
- AI agents cannot silently exceed their intended authority
AI can accelerate implementation. It cannot decide which assumptions are safe for your users and your production environment.
What security check has caught the most surprising issue in one of your projects?
Top comments (1)
AI-built apps need the same boring security checks as hand-built apps, but with more suspicion around defaults. The risk is that generated code can look coherent while quietly skipping ownership boundaries, RLS assumptions, or edge-case auth paths.