Skip to content
Back to field notes
APIsJune 6, 20266 min read

Serverless function exposure in AI-built apps: which routes are public by default

Every file you drop in an API folder becomes a public HTTP endpoint, and AI tools generate them without auth guards. This note shows how to find the routes that answer anyone before you share the URL.

APIsServerlessAuth

A file in the API folder is a public endpoint

The concrete launch risk is this: a file you placed in pages/api/, app/api/, server/api/, or netlify/functions/ is a live HTTP endpoint the moment you deploy, reachable by anyone who knows or guesses the path. Nothing about being "serverless" makes it private. The framework maps the filename to a URL and serves it to the internet. If the function does an admin job, deletes a record, sends an email, or returns user data, it does that job for whoever calls it unless the function itself checks who is calling.

That is the part AI-built apps miss. The generated handler usually contains the business logic and nothing else. It reads a request, talks to the database, and returns a result. There is no line that asks whether the caller is logged in or allowed to perform the action. The route works in the app because the UI only shows the button to a signed-in user, but the endpoint underneath does not know about the UI.

Separate what is observable from what is only possible here. Observable, from outside, with no attack: the route exists and returns a non-empty response to an unauthenticated request. That is reproducible. What is only possible, and needs you to read the code, is whether that response leaks data it shouldn't or performs a privileged write. The public probe tells you a door opens; it does not tell you what is behind every door.

Why AI-built apps ship open functions

File-based routing is the default in the frameworks these tools generate. In Next.js, a route handler under app/api/ or a file in pages/api/ is published as soon as it exists. In Nuxt, files under server/api/ become Nitro endpoints automatically. On Vercel and Netlify, a function in the functions directory gets a public URL on deploy. None of these require an auth check to make the route live. The route being public is the framework working as designed; the missing guard is the gap.

When you prompt an AI tool to "add an endpoint that returns the user's orders" or "make an API to delete a project," it writes the handler that does exactly that. It rarely writes the surrounding authentication and authorization, because that depends on your session setup, your roles, and your data model, which the prompt did not describe. The generated code compiles, the happy path works in the browser, and the app looks finished.

  • The function works through the UI, so the missing guard is invisible from the inside.
  • Authentication (who you are) and authorization (what you may do) are two separate checks, and AI-generated handlers often have neither.
  • Fast deploy means the public URL exists before anyone enumerates which routes are reachable. This is a common generated-insecure-default; see also unauthenticated API endpoints in AI-built apps.
  • Admin and internal operations are the dangerous case: a route that bulk-deletes, exports data, or changes a role is callable by anyone if it only relied on the UI to hide the button.

Find your own reachable routes before launch

You can confirm this on your own app without attacking anything. The goal is to list your serverless routes and see which ones answer an anonymous caller.

Start from the source. Look at the files in pages/api/, app/api/, server/api/, or your platform's functions directory. Each file is a route. Write down its public path and the HTTP methods it handles. AI-generated projects often also leave convention routes that you did not write yourself, so include anything in those folders, not just the ones you remember.

Then probe from a clean browser session, signed out, with DevTools open on the Network tab. Visit each route directly in the address bar for a GET, and for routes that act on data, use a simple unauthenticated POST from the terminal with curl to your own URL. Watch the status code and the body.

  • Suspicious result: a 200 with a real JSON body, a list of records, an object with user fields, or a success message confirming an action. That means the endpoint ran for an anonymous caller.
  • Safer result: a 401 or 403, a redirect to a login page, or an empty deny response for any route that should require a session. That is the endpoint refusing an unauthenticated request, which is what you want.
  • Treat write routes with care: test destructive endpoints against throwaway data in your own project only, because a successful unauthenticated POST or DELETE proves the action ran, and the action is real.
  • Repeat per route. Guards are per-handler, so one protected route tells you nothing about the next file in the folder.

Add the guard inside the function, not just the UI

The fix lives in the handler. Every serverless function that touches data or performs an action must verify the session and the permission at the top, before it does anything else, and reject the request if either check fails. Hiding the button in the frontend is not a control; the endpoint is reachable regardless of what the UI renders.

Concretely: read the session or token on the server inside the function, confirm the caller is authenticated, then confirm the caller is authorized for this specific action and this specific record. A logged-in user being authenticated does not mean they may delete another user's project, so the authorization check has to compare the caller against the resource, not just confirm that someone is logged in. Where many routes need the same check, put it in shared middleware or a helper the framework runs before the handler, so a new route is closed by default rather than open by default.

  • Authenticate first, then authorize the specific action against the specific resource.
  • Make the safe state the default: a route with no explicit guard should deny, not serve.
  • Keep privileged keys server-side; a function using a service-role or admin key must never be callable without an auth check, because it bypasses your data rules.

Adding an auth guard stops anonymous callers, but it does not by itself fix what the function returns once a real user is in. Over-fetching, returning fields a user should not see, or letting one tenant read another's rows are authorization and data-shaping problems that live in the response body and the query, and they need their own review even after the route requires a login.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check probes known serverless route conventions from the public surface and reports routes that return a non-empty response to an unauthenticated GET or POST. That is the observable signal: from outside, with no credentials, a route that answers is a route worth investigating before you launch. The scan flags it with severity and fix direction so an open endpoint shows up in your own check rather than someone else's.

What it does not do is read your code or judge your logic. It cannot confirm whether a guarded route authorizes the right users, whether a response over-fetches, or whether a write enforces the correct ownership rule. Those are authorization and business-logic questions that need manual review or code inspection. The same is true for routes that only appear under unusual paths the scan does not guess; the file list from your own source is the authoritative inventory.

Next step: list every file in your API and functions folders, then go down the list and confirm each route either denies an unauthenticated request or is genuinely meant to be public. Run a focused Launch Check against the public URL before sharing it, so an open serverless function shows up in your scan first.

A clean Launch Check on the public surface does not prove every function is correctly guarded. The probe sees what answers an anonymous request along known conventions; it does not enumerate every route you wrote or verify that an authenticated request is properly scoped. Treat a flagged route as a launch blocker, and treat a clean result as one signal, not a guarantee.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.