Every service gets its own public URL by default
The concrete risk on Render and Railway is that a service you think of as internal is reachable from the open internet the moment it deploys. Both platforms assign a public URL to a web service by default, before you have decided whether that service should face the world. So when your app is more than one service, each extra service can come up with its own hostname that anyone can hit.
That matters because multi-service apps usually have a service that was never meant for the public: an admin API, a worker with a health or metrics endpoint, an internal jobs dashboard, or a database UI like Adminer or pgweb spun up "just to look at the data." The frontend has a URL on purpose. The admin API often has one by accident.
Separate what is observable from what is only possible. Observable: a service has a public hostname and returns a response to an unauthenticated request. Only possible: that the response lets someone actually do something harmful. A health endpoint that returns 200 OK is exposed but low-impact; an admin route that lists users or accepts a write without a login is exposed and serious. The check below is about which of your services answer at all, and what they say when no one is logged in.
Why AI-built multi-service apps hit this
When you ask an AI tool to build something with a frontend, an API, and a background worker, it scaffolds them as separate services and wires up a deploy config that gets all of them running. The generated config optimizes for "everything is up and the app works," not for "only the frontend is public." Getting the worker reachable is the path of least resistance, so it tends to inherit a public URL like everything else.
The platform default does the rest. Render exposes a web service publicly unless you choose a private service type; Railway gives a service a public domain when you generate or enable one, and services can also talk to each other over private networking that you have to opt into. The AI-generated flow rarely opts into the private path on its own, because the public path is what makes the demo work on the first try.
- The app works end to end, so the extra public surface is invisible from inside the product.
- Admin and worker services are added late and fast, often with no auth middleware, because they were "internal."
- Fast deploy means every service has a live URL before anyone audits which ones should. This is the same exposed-admin-routes pattern that shows up in AI-built apps generally.
Find your own public service URLs
You can confirm this on your own deployment without attacking anything. Work from two sides: your platform dashboard, and the public URLs themselves.
From the dashboard: open your Render or Railway project and list every service. For each one, note whether it has a public domain or generated URL attached. On Render, check the service type, because a service marked as a private service has no public address. On Railway, look at whether a public domain is enabled and whether the service relies on private networking to reach its peers. The suspicious result is any backend, worker, admin, or database-tool service that has a public URL it does not need.
From the public surface: take each service URL and make a plain unauthenticated request, only against your own services. Open the URL in a browser or send a simple GET and read what comes back.
Be precise about what a finding means. A reachable URL is an authentication problem: the service let an anonymous caller in. Whether that caller can then read or change data is an authorization question, and the answer depends on what the service does once it has answered. Treat both as worth fixing, but do not assume a 200 from a health check is the same severity as a 200 from an admin list.
- Safer result: the request is refused, redirected to a login, returns 401 or 403, or the service has no public URL at all. That means the door is closed to anonymous callers.
- Suspicious result: an admin-style page or JSON response loads with no login, a health or metrics path like /health or /metrics returns internal detail, or a database UI renders. Anything that answers usefully without auth is the finding.
- Check the obvious internal paths too, on your own services only: a root path that lists routes, an /admin area, an API that returns records, or a status endpoint that leaks versions and config.
Make internal services private, then add auth
The fix has two layers, and the first is the one that removes the exposure entirely.
Stop publishing services that do not need a public URL. On Render, run a backend that only your frontend calls as a private service so it has no public address, and let your services reach it over the internal network. On Railway, use private networking between services and do not enable a public domain on the ones that should stay internal; reference them by their internal hostname instead of a public URL. A service with no public URL cannot be hit by an anonymous request from the internet, which is the cleanest outcome.
For any service that genuinely must be public, put authentication in front of it. An admin API needs a real login or a strong token check on every route, not security by obscure URL. A health or metrics endpoint should either require auth or return nothing sensitive. Set environment variables and secrets per service so an internal service is not shipping credentials into a public bundle.
- Move admin, worker, and database-tool services to private networking or a private service type so they have no public hostname.
- For services that must stay public, require auth on every route, including health, metrics, and status paths.
- Confirm the change from outside: after making a service private, the old public URL should stop answering.
Making a service private closes the front door from the public internet. It does not, by itself, fix authorization inside the service. Code that trusts any caller because "it is only reachable internally" can still be wrong if another service, or a future public route, calls it. Private networking reduces exposure; it is not a substitute for an auth check on sensitive actions.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check probes the public URLs of your services from the outside. It looks for services that answer unauthenticated requests with admin-style responses, for open health and status endpoints, and for missing auth on a surface that should require a login, then reports each finding with severity and fix direction. Point it at a service URL and it tells you what an anonymous visitor reaches, which is exactly the gap a public-by-default platform creates.
What it does not do is map your private networking or read your deploy config to confirm which services are meant to be internal. It sees what responds on the public surface; it cannot tell, from outside, whether a reachable admin API has a real authorization model behind its login, because that is a code and business-logic question, not a public-surface one.
Next step: open your Render or Railway project, write down every service and whether it has a public URL, and make the ones that should be internal private. Then run a focused Launch Check against each remaining public URL before you share the launch link, so an exposed admin or worker endpoint shows up in your scan and not in someone else's.
A clean Launch Check means the service URLs it tested did not hand an admin response to an anonymous caller. It does not prove every internal service is private, that authorization is correct once a user is logged in, or that a service you did not give it has no public URL. List every service yourself and confirm each one's exposure.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.