DEV Community

Peter
Peter

Posted on

7 Security Checks Before Shipping an AI-Built Next.js + Supabase App

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);
Enter fullscreen mode Exit fullscreen mode

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:

  1. An authenticated user can access their own row.
  2. That user cannot access another user’s row.
  3. An unauthenticated request is rejected.
  4. 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 (6)

Collapse
 
alexshev profile image
Alex Shev

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.

Collapse
 
peterbuildssecure profile image
Peter

That's a good way to frame it — the defaults are exactly where I'd put the suspicion. RLS 'enabled' reads as safe and isn't, a service-role key defaults to full access, CORS configs get copy-pasted with credentials left on. The pattern I keep running into is that AI tools optimize for 'the demo works,' and nothing in that loop asks 'what does this let the wrong caller do' — that question still has to come from a human reviewing the generated code, not the tool itself.

Collapse
 
alexshev profile image
Alex Shev

Exactly. The dangerous part is that each default has a reassuring name: RLS, service role, CORS, callback URL. They sound like controls, so generated code tends to treat their presence as proof. I like making the audit ask a more annoying question: what would have to be true for this setting to be safe in production?

Thread Thread
 
peterbuildssecure profile image
Peter

That question works well because it forces a specific answer instead of a checkbox. For service-role key: 'safe' requires it never being reachable from any code path the client can trigger, not just 'not in the .env.local that's committed.' For RLS: 'safe' requires a passing adversarial test per policy, not just ENABLE ROW LEVEL SECURITY being present. For CORS: 'safe' requires the allowed origins list to be finite and reviewed, not *. In each case the setting existing tells you someone thought about it once — it doesn't tell you the answer is still true today.

Thread Thread
 
alexshev profile image
Alex Shev

That is the right shape of the audit. The setting existing is just the starting point; the proof is whether the wrong caller can actually cross the boundary. I like phrasing those checks as adversarial questions because it forces the test to name the attacker, not just the feature.

Collapse
 
alexshev profile image
Alex Shev

Exactly. The demo-working loop is dangerous because it rewards the first happy path that compiles. Security review has to ask a different question: what new authority did this code create, and who can reach it? Defaults are where that authority hides.