In a Lovable- or Bolt-style app, any server route that calls a paid model and skips the login check can be called by anyone who finds its URL, and every call bills your account. Find those routes by listing every server route, then marking the ones that call a model and the ones that check who is signed in. A route that does both, and has no check, is the one to close first.
A developer read 25 public vibe-coded apps, mostly Lovable, by reading their code and git history. None had a hardcoded secret. Eleven had any server layer at all; the other fourteen were frontend-only. Two of the eleven exposed an AI-calling route with no login check and no per-user rate limit. The author calls that "n=2, not a statistic," and the sample is small. Treat it as a checklist of where to look, not as a market study.
What the scan found, and what it did not
The useful part of the sample is the shape of the two failures. One was a pair of Supabase edge functions with verify_jwt set to false and CORS set to *. The other was three Next.js API routes with no middleware. In both, the route worked for anyone who had its address. The article also names two more patterns to check for: verify_jwt = false combined with the service role key, and loose row-level security policies.
The sample does not tell you how common this is across all apps, or how your own app compares. It shows where to look.
Find the routes that spend money
-
List every server route. In Supabase, that means the edge functions under
supabase/functions/. In Next.js, that means the handlers underapp/api/and any other route handlers. In Express, that means every route registration. -
Check the login gate on each route. In Supabase, look for
verify_jwtinsupabase/config.toml, and check whether the function reads the signed-in user from the request. In Next.js, check which pathsmiddleware.tscovers, and whether the handler checks the session itself. In Express, check whether auth middleware sits on the route. -
Mark the routes that call a model. Look for requests to the OpenAI, Anthropic, or OpenRouter APIs, or a direct
fetchto their URLs. A route that passes the request to another internal function still counts if that function calls a model. - Fix every route that is both signed out and model-calling. Require a signed-in user before the model call, then add a per-user limit so one account cannot run up the bill.
- Keep the service role key off reachable routes. A service role key bypasses row-level security. If a route that a signed-out visitor can reach uses it, that route can read or change any row.
Keep the list in a file your team reviews. A new route added later without the check is the usual way this problem comes back.
Conceptual comparison of public and signed-in route paths, each reaching a paid model; not an OTF feature.
Why model routes cost more than other routes
Most routes in a small app cost almost nothing per call: a database read, a form save, a page load. A route that calls a model is different. Each request sends tokens to a paid provider and returns a bill line you do not control in the moment. A script that loops on that route can run up spend quickly, which is why it needs its own check.
That is why the check belongs on the model call itself, not only on the page that happens to use it. A signed-in page that calls an unprotected route is still an open door for anyone who finds the route URL directly. Treat the route as the unit of protection.
Who should own the check
The check should live in one place that every model-calling route passes through, rather than being copied into each handler by hand. A shared helper that reads the signed-in user, applies the limit, and then calls the model is easier to review than ten separate copies. When a new route needs model access, the review question is simple: does it go through the helper?
Give the helper a short test suite. One test should send a request with no session and expect a refusal. Another should send a request over the limit and expect a clear error. Run both in CI so a future change cannot quietly remove the check.
Hypothetical spend-leak illustration; no OTF product behavior or measured usage is shown.
Test the open door from outside
Read the code first, then test the deployed app. From a machine with no session, send a request to each model-calling route and confirm it is refused. The request should carry no cookie and no bearer token. A reply that returns model output, or a rising usage count on your provider's dashboard, means the route is open.
Test again after every deploy. Do not assume that a fix on one route covered its siblings. Both failures in the sample were in routes that looked protected until someone read them.
Add a per-user limit, not only a login
A login check stops anonymous callers. It does not stop a real account from making thousands of calls. A per-user limit caps how much one account can spend in a window. Put the counter on the server, keyed by the user ID from the verified session, and reject calls over the limit with a clear message. Store the counter where a visitor cannot reset it, such as the database rather than the browser. For the wider setup, see rate limiting for an owned API in production.
Pick the limit from your own cost model: what one normal session costs, and how much you can lose before someone looks at it. That number is a product decision, not a default from a template.
Where the kit fits, and where it does not
The SaaS Dashboard kit is a Vite + React 19 app with email sign-in, its own typed API, and a PostgreSQL database. That gives you a login layer to build on. It does not include AI routes, and its product page does not document per-user rate limiting. It is not a security review of your app. If you add an AI route to it, steps 2 to 4 above still apply to that route.
You can click through the running app at the live demo before deciding whether its login layer fits your project. For a wider pre-launch pass, see the AI app security checklist.
A short checklist before launch
- List every server route, including edge functions and API handlers.
- Mark each route that calls a paid model.
- Confirm every marked route requires a signed-in user on the server.
- Add a per-user limit on every marked route.
- Keep the service role key off any route a signed-out visitor can reach.
- Test each model-calling route from outside with no session, and confirm the refusal.
Sources
- DEV Community, "I scanned 25 vibe-coded apps. The API keys were fine. The server wasn't." by ad23dev, posted 1 Nov 2026, read 2 Nov 2026: https://dev.to/ad23dev/i-scanned-25-vibe-coded-apps-the-api-keys-were-fine-the-server-wasnt-1lc6
- SaaS Dashboard kit scope, read 2 Nov 2026: https://otf-kit.dev/md/templates/saas-dashboard.md


Top comments (0)