Dev.to AI 🤖 Ai 👁 0 📖 4 min read

It works locally and dies in production: the environment variables AI never writes down

Every week somebody sends me an app with the same sentence: "it works on my machine". It usually does. The gap is almost never the code — it is the dozen values that exist as a .env on one laptop and nowhere else, becaus

Every week somebody sends me an app with the same sentence: "it works on my machine". It usually does. The gap is almost never the code — it is the dozen values that exist as a .env on one laptop and nowhere else, because the AI that wrote the app never had to deploy it.

This is the list I walk through before a deploy, in the order that stops the most breakage. If you are shipping an app you generated with Cursor, Lovable, Bolt or v0, half of these are missing from your repository right now.

1. The public URL of the app itself

Auth emails, password resets, OAuth redirects and share links all need to know where the app lives. Generated code usually hardcodes http://localhost:3000.

  • NEXT_PUBLIC_SITE_URL / VITE_APP_URL / PUBLIC_BASE_URL
  • Use the production domain, no trailing slash, and keep a separate value per environment.

2. Auth redirect allow-list (not an env var, but the same class of bug)

Supabase, Firebase Auth, Auth0, Clerk and Google OAuth each keep their own list of allowed callback URLs. If the deployed domain is not on it, sign-in fails with a generic error and you lose an hour. Add apex, www, and every preview domain you actually use.

3. The keys that must never reach the browser

A client-exposed prefix — NEXT_PUBLIC_, VITE_, PUBLIC_, EXPO_PUBLIC_, REACT_APP_ — inlines the value into the JavaScript bundle. That is fine for a publishable key and fatal for a secret.

Check what actually shipped:

grep -r "NEXT_PUBLIC_" .env* src/ | grep -iE "secret|service|private|admin"

Anything that matches should move to a server-only variable and be called through an API route or edge function.

4. Database connection details, including the pooler

Serverless functions open a connection per invocation. A direct Postgres URL (db.<project>.supabase.co:5432) exhausts connections under a modest load; the pooler URL (...pooler.supabase.com:6543) is the one production wants. Most generated .env files list only the first.

5. Payment keys, and the webhook secret separately

Stripe, Toss, PortOne and Lemon Squeezy all need at least three values: a publishable key, a secret key, and a webhook signing secret that differs per environment. A missing webhook secret does not fail loudly — it fails silently, and the customer pays without getting the thing.

stripe trigger checkout.session.completed   # with --forward-to localhost for local test

Run one real test purchase in production before announcing anything.

6. Transactional email

A provider key plus a verified sending domain. If the sending domain is not verified, mail goes out from a shared address and lands in spam. This is the single most common "the app doesn't work" report I get from non-technical users.

7. Object storage

Bucket name, region, and the credentials or signed-URL secret. A bucket that exists only in the development project returns 403 in production and the upload spinner runs forever.

8. The AI provider key, with a spending cap

If the app calls an LLM, maps, or a vision API, the key goes on the server and the account gets a hard limit and an alert. A retry loop on a client-side call is how a weekend turns into an invoice.

9. Cron and background jobs

A nightly job that runs on your laptop does not run in production. Whatever triggers it — Vercel Cron, Supabase pg_cron, a GitHub Action, an external scheduler — needs its own config plus a secret so the endpoint is not world-callable.

10. Feature flags and analytics

Missing keys here do not break the app, they break your ability to see it. Add one event per important action before launch; retrofitting analytics after users arrive means the first week is invisible.

11. Error reporting

Sentry (or similar) needs a DSN, and it needs to be deployed before things break, not after. A production app with no error tracking is a black box with a payment button.

12. Legal and contact values

Privacy policy URL, support email, business name and address — the values the store reviewer and the first paying customer look for. Hardcoding your personal Gmail as the support address is fine until the first refund request.

The five-minute version

# 1. what does the code expect?
grep -rhoE "(process\.env\.[A-Z0-9_]+|import\.meta\.env\.[A-Z0-9_]+)" src/ app/ | sort -u

# 2. what does production actually have?
# (paste into your host's env settings and diff the two lists)

# 3. what leaked into the bundle?
grep -rlE "NEXT_PUBLIC_(SECRET|SERVICE|PRIVATE)" .next/ 2>/dev/null

Diff the first list against the second, and you have your deployment bug list. It is unglamorous work, and it is exactly the part an AI will not do for you: the model optimises for code that runs, and a deployment is a set of facts about an environment it cannot see.

That is also the honest answer to "why does finishing take longer than generating": the last mile is not more features, it is making the same app work for someone else, on another machine, with a real card and a real inbox.

CPC AI Studio (CPC AI Inc., Seoul) finishes AI-built apps — deployment, auth, payments, store submission. 355 projects to date, 3–5 days for a small app.
Studio: https://cpcai-studio.netlify.app/en/ · Contact: [email protected]

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.