Skip to content
ShipSecurelyShipSecurelySecurity reference
Menu

23 tools · 102 fixes · 28 issue guides · Sources: GitHub, Supabase, OWASP, Vercel + 28

Some links on this page are affiliate links. If you use one, we may earn a commission, at no extra cost to you. Affiliate and sponsor relationships do not change comparison or benchmark order. Disclosure

Issues

How to deploy your vibe-coded app securely

Put server secrets in the host's environment, keep them out of the browser bundle, and do not give a preview deploy the production database or a public URL it does not need.

Share link · Share on X

By ShipSecurely Editorial · Updated · How we make these guides

highSecrets

Shows up in: Next.js, Vite, Railway, Vercel, Render, Fly.io

What it is

A vibe-coded app usually deploys the same way it was built: the agent writes a .env, the host imports it, and a preview URL appears for every pull request. The security work is deciding which of those values the browser is allowed to see, which network path can reach the database, and which key a preview is allowed to hold.

Server-side environment variables are values the running server process can read. They are not automatically safe. Next.js inlines any name that starts with NEXT_PUBLIC_ into the browser bundle at build time. Vite does the same for names that start with VITE_, through import.meta.env. A secret with either prefix is in the JavaScript you ship. The check for a key that already leaked is Secret keys shipped in the frontend.

Private networking is how two of your own services talk without a public address. Preview hygiene is the rule that a pull-request deploy gets test credentials, not the production database password. Least privilege means the key you put in CI can deploy one environment, and the key in the app can only do the job that code does. A Stripe restricted key and a Supabase publishable key are that idea. A host account token that can see every project is not.

The four hosts below are in alphabetical order. One Railway link on this page is an affiliate link. That does not change this order, a comparison table, or a benchmark.

How to check in 5 minutes

  1. Search the repo for NEXT_PUBLIC_ and VITE_ next to SECRET, SERVICE_ROLE, sk_live_, DATABASE_URL, and BEGIN PRIVATE KEY. A secret must not use a public prefix.

  2. Build the app and search the output JavaScript for those same strings. Dev mode can hide a value the production bundle still inlines.

  3. In the host dashboard, open Production and Preview side by side. If the preview database URL or secret key is the production value, stop and split them.

  4. List every service with a public URL. A database, a worker, or an internal API that only other services call should not have one.

How to fix

Next.js and Vite

Next.js documents that variables without NEXT_PUBLIC_ are available in the Node.js environment and are not accessible in the browser. The NEXT_PUBLIC_ prefix tells Next.js to inline the value into the client bundle at build time. After the build, changing that variable does not change the shipped JavaScript until you build again.

Vite documents the same boundary with a different prefix. A name starting with VITE_ is exposed on import.meta.env and bundled into client code. Vite's env guide says those variables should not contain API keys. DB_PASSWORD without the prefix stays off the client. Lovable and Bolt apps are often Vite apps, so a VITE_ secret is the same bug as a NEXT_PUBLIC_ secret.

Read secrets from process.env in a Route Handler, a Server Action, or a separate backend. Do not pass them into a client component as props. Do not commit the file that holds the real values. Commit an example file with empty values, and keep .env and .env.local out of git. See Keys committed to GitHub.

Fly.io, Railway, Render, and Vercel

Fly.io. Put credentials in fly secrets, not in the [env] table of fly.toml. Fly's configuration reference says [env] is for non-sensitive values, and that secrets take precedence over an env entry with the same name. Secrets are stored in an encrypted vault and injected as environment variables when a Machine boots. They are not in the image unless you deliberately use build secrets. Apps in the same organization share a private IPv6 network Fly calls 6PN. Another app is reached at <app-name>.internal. That network is the whole organization, not one environment: a staging app and a production app in the same organization can address each other. Put them in separate organizations if they should not. An app with no services or http_service section is not reachable from the public internet.

Railway. Variables on a service are available to the build and to the running deployment. A build command that prints the environment can write a secret into the build log, so do not echo them. Sealing a variable keeps it available to builds and deploys, but the dashboard and API cannot read it back. Railway does not copy sealed variables into pull-request environments, duplicated environments, or duplicated services. Give a PR environment its own test credentials. Services in the same project and environment reach each other at <service>.railway.internal over WireGuard, and a browser cannot open that name. The private network is isolated per environment, so production cannot reach staging over it. For CI, use a project token scoped to one environment in one project. An account token can see everything your user can see. Do not put an account token in shared CI. Railway (affiliate). New users get $20 in Railway credits with our link. Railway's affiliate docs describe that credit as equivalent to a free month on the Pro tier.

Render. Set variables on the service's Environment page. If you describe the service in a Blueprint, mark a secret with sync: false so the value is entered in the dashboard and is not committed in render.yaml. Preview environments can override a variable with previewValue. That is the place for a test API key. Render's preview docs say placeholder variables with sync: false are not copied into preview environments. Services in the same region can use the private network and an internal hostname. Render documents blocking private traffic between environments on Pro workspaces and higher, not on every account. Prefer the database's internal connection string, and do not leave a public access list open when only other services need the database.

Vercel. Each variable can be limited to Production, Preview, Development, or a single preview branch. A branch-specific preview value overrides the shared preview value of the same name. Use that to point a branch at a preview database. Do not prefix a secret with NEXT_PUBLIC_. Preview deployments should not receive the production database URL or a live secret key. Private connectivity from Vercel Functions to a network you run is Secure Compute. Vercel documents Secure Compute as an Enterprise feature. It is not the default. A normal Vercel project does not put your function on a private network with your database. IP allowlisting still needs a password or a key. The address alone is not access control.

Copy the fix prompt

Paste this into Cursor, Lovable, Bolt, or Claude Code. Read the diff before you merge it.

You are checking how this app is deployed. Do not add features. Do not print secret values.

Search for NEXT_PUBLIC_ and VITE_ next to SECRET, SERVICE_ROLE, DATABASE_URL, sk_live_, and private keys. A secret must not use those prefixes.

Then tell me, without values:
1. Which env names are safe to be public, and which must stay on the server.
2. Whether a preview or pull-request environment is likely to receive the production database URL. If the repo cannot answer that, say so and name the host dashboard page I should open.
3. Which services should not have a public URL. For Fly.io, Railway, Render, or Vercel, name the private-network hostname pattern from the host's docs only if this repo already uses that host.

Do not add an affiliate link. Do not change comparison order or a benchmark.

Questions

Can a preview deploy use the production database?

No. A preview URL is there so a change can be clicked before it is merged. Railway does not copy sealed variables into pull-request environments. Render can set a separate previewValue, and it does not copy sync: false secrets into previews unless you put them in a shared group on purpose. Vercel lets you scope a variable to Preview instead of Production. Use those mechanisms. A preview that holds the production database password is a second production.

Do I have to change hosts to do this?

No. The checks are the same on Fly.io, Railway, Render, and Vercel. The host changes where you type the variable and whether private networking is on by default. Pick the host the app already deploys to. An affiliate link on this page does not change that.

Sources

Related

The weekly fix

The weekly fix: 3 real security mistakes in vibe-coded apps and how to fix them, every Tuesday.

One email, every Tuesday. A welcome note follows the first signup.