Skip to content

feat(env): four env targets — localhost, dev, uat, prod#33

Merged
samutpra merged 20 commits into
mainfrom
feat/env-modes
Jul 15, 2026
Merged

feat(env): four env targets — localhost, dev, uat, prod#33
samutpra merged 20 commits into
mainfrom
feat/env-modes

Conversation

@samutpra

@samutpra samutpra commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Restructures the Vite env-mode setup into four honestly-named targets. No src/ changes — the whole branch is package.json, vite.config.ts, and docs.

The four targets

script mode file backend
bun start, dev, dev:local localhost .env.localhost http://localhost:4000
dev:dev dev .env.dev https://dev.blueledgers.com:4001
dev:uat uat .env.uat https://api-carmen-web.pncsb-app.com
dev:prod prod .env.prod https://dev.blueledgers.com:4001 ⚠️ placeholder

build:* mirrors dev:*dev:X and build:X read the same file. All 12 scripts pin an explicit --mode.

What this fixes

An inherited misnaming. Mode prod held the DEV backend (the original .env.production was documented as "deployed dev backend"). So dev:local was running against dev.blueledgers.com, and dev:prod lied about where it pointed. Now each target has its own honest slot.

A silent-failure class. vite.config.ts now throws if REACT_APP_API_BASE_URL or REACT_APP_API_APP_ID is missing, replacing a || 'http://localhost:4000' fallback. The real bug was worse than that fallback suggested: api.ts reads import.meta.env.REACT_APP_API_BASE_URL directly, so a missing env file produced a complete, exit-0 bundle running axios.create({ baseURL: undefined }) — every request silently resolving against whatever origin served the SPA.

Adds the UAT target (.env.uat, dev:uat/build:uat). CORS verified live: the UAT backend answers preflight from http://localhost:3304 with 204 + access-control-allow-origin: *. No backend change needed.

Why the mode is localhost and not local

Vite hard-throws on a mode named local — it conflicts with the .local env-file postfix. That's why this repo originally used the awkward development/production names. Script names are free-form, so dev:local still works; it maps to --mode localhost.

Related, and the reason the docs warn about it: a bare .env or .env.local is not a mode file. Vite loads both in every mode, so either silently leaks across all four targets and satisfies the new guard.

⚠️ prod is a placeholder

Production isn't stood up yet, so .env.prod points at DEV. It still sets REACT_APP_ENV=production deliberately: .env.prod is production's config slot, so production is the correct steady-state label — the URL is the placeholder, not the label. The alternative would need a future flip that gets forgotten, leaving real production badged development.

Accepted risk: during this window the badge reads · production while the app talks to DEV. .env.example carries the warning, since the badge can't. When production exists: swap the URL and app id, delete the note. Nothing structural moves.

Blast radius is local only — CI and Vercel supply REACT_APP_* via process env, which loadEnv merges at higher precedence, so the placeholder can never reach a real deploy.

Before merging

vercel.json runs npm run build--mode prod. .env.prod is gitignored, so the guard needs both REACT_APP_ vars set as Vercel project env vars. If they're missing the first deploy after merge fails loudly — the guard working as designed, but a surprise. (If Vercel deploys currently succeed, they're already set.) GitHub workflows are verified fine.

Testing

453/453 Vitest, no src/ changes. Each of the four modes verified by grepping the built bundle for the inlined backend URL; bare npx vite build verified to throw and emit zero assets; loadEnv('development') and loadEnv('production') — Vite's bare defaults — both verified to resolve to nothing, so there's no leak path around the guard.

Known flake, pre-existing and unrelated: NewsManagement.test.tsx can fail under parallel load (15/15 isolated).

🤖 Generated with Claude Code

samutpra and others added 20 commits July 15, 2026 10:15
Adds a third Vite mode (`uat`) backed by `.env.uat`, plus `dev:uat` and
`build:uat` scripts, so the local dev server and local builds can target the
UAT backend. Local-only: no CI deploy, no infra, no source changes.

Documents the CORS constraint — api.ts uses an absolute baseURL, bypassing the
Vite proxy, so the UAT backend must allowlist http://localhost:3304.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two tasks: (1) .env.uat + dev:uat/build:uat scripts, verified by grepping the
built bundle for the inlined UAT host; (2) document the mode in .env.example
and CLAUDE.md, which are the only committed record since .env.uat is ignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a third Vite mode targeting the UAT backend. vite.config.ts is already
mode-generic via loadEnv(mode, ...), so no config or source change is needed —
only the scripts and a gitignored .env.uat file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.env.uat is gitignored, so these two files are the only committed record of
how to recreate it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.env.example documented only UAT's base URL, so copying it paired UAT with the
DEV app id hardcoded in the file body — requests would be rejected by AppIdGuard.
Document UAT's own app id and REACT_APP_ENV. App ids are public identifiers: the
DEV one is already committed here and api.ts ships it as a plain header.

docs/DEVELOPMENT.md was missed by the spec and still enumerated exactly two
modes; mirror the CLAUDE.md edits into it.

Also correct the "(proxied in dev)" claim: api.ts uses an absolute baseURL, so
the vite.config.ts proxies never fire for service calls in any mode, and every
mode depends on backend CORS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renaming .env.development/.env.production means renaming the Vite modes, since
Vite derives the filename from the mode. Verified against Vite 8.1.0 before
designing: bare vite/build/preview work today only because the filenames match
Vite's default mode names, so 8 scripts must gain an explicit --mode. The
suspected NODE_ENV flip was refuted by build probe — NODE_ENV comes from the
command, not the mode name.

Adds a fail-fast guard for the two required REACT_APP_ vars, replacing the
silent localhost fallback. loadEnv merges process.env, so CI is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three tasks: (1) fail-fast guard, landing first so the rename never has a
silent-failure window — RED proves the current localhost fallback hides a
missing env file, GREEN proves the guard throws; (2) rename the files on disk
and pin --mode on all 9 scripts; (3) docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vite.config.ts fell back to http://localhost:4000 when REACT_APP_API_BASE_URL
was absent, so a missing .env file silently produced a bundle pointed at
nothing. Require both REACT_APP_API_BASE_URL and REACT_APP_API_APP_ID (both are
mandatory — api.ts sends the app id as x-app-id and the backend enforces it) and
throw naming the mode and expected file.

loadEnv merges prefixed process.env vars, so CI is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vite derives the env filename from the mode, so renaming .env.development ->
.env.dev and .env.production -> .env.prod is a mode rename. The old filenames
matched Vite's default mode names, which is the only reason a bare `vite`,
`vite build`, or `vite preview` picked up an env file; every script now passes
--mode explicitly.

uat is unchanged. CI is unaffected: loadEnv merges prefixed process.env vars, so
the workflows never depended on an .env file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
.env.dev and .env.prod are gitignored, so these three files are the only
committed record of the modes. Also documents that every script must pass --mode
explicitly, since Vite's default modes no longer match any file.

Also corrects two errors in the spec/plan for this branch: the vite.config.ts
apiTarget fallback only ever fed the dev-server proxy (never the client bundle),
so removing it doesn't fix a build-time silent failure — the real one is
src/services/api.ts:4 inlining REACT_APP_API_BASE_URL as undefined; and
.env.dev/.env.prod carry no REACT_APP_ENV key at all (only .env.uat sets it),
so the Login/Landing badge only ever renders under uat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The header line was padded to satisfy an alignment check that accidentally
scoped over it along with the five data lines it was meant to align. The check
was wrong; the ~40-char gap read as a typo. Data lines 2-6 are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vite loads a bare .env in EVERY mode — like the .env.local the docs already warn
against — and it satisfies the new required-env guard. So `cp .env.example .env`
gave a fresh clone all three modes silently pointed at the deployed DEV backend,
with no signal distinguishing dev:local from dev:prod and no uat badge: the exact
silent misconfiguration this branch exists to prevent.

Document per-mode copies instead, retarget the .env references at the mode file,
warn against bare .env alongside .env.local, and annotate preview's mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Troubleshooting still told readers to check REACT_APP_API_BASE_URL "in .env" —
the one reference the setup cleanup missed, and one that contradicts the same
file's warning never to create a bare .env.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersedes the two-mode scheme on this branch. Four honestly-named targets, each
reachable by dev:X and build:X. Mode `local` is illegal in Vite (verified: it
throws, conflicting with the .local postfix), so the local-backend mode is named
`localhost`; script names are free-form so dev:local still works.

Fixes an inherited misnaming: mode `prod` held the DEV backend. prod now has its
own slot, pointing at DEV as a documented placeholder until production exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two tasks: (1) mv .env.local -> .env.localhost, add REACT_APP_ENV to the existing
.env.prod, 12 scripts, guard hint — RED shows dev:local currently resolving to the
DEV backend and dev:dev missing; (2) docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds dev:dev/build:dev and splits the local backend onto its own mode, fixing an
inherited misnaming: mode `prod` held the DEV backend, and `dev:local` therefore
ran against dev.blueledgers.com rather than localhost.

The local mode is named `localhost` because Vite throws on a mode named `local`
(it conflicts with the .local postfix). Script names are free-form, so dev:local
still works.

prod points at DEV as a documented placeholder until production is stood up; it
keeps REACT_APP_ENV=production because .env.prod is production's config slot —
the URL is the placeholder, not the label.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The four env files are gitignored, so these three files are the only committed
record of them. Includes the loud PLACEHOLDER note on .env.prod: it points at the
DEV backend while badging `· production`, and .env.example is the only place that
can warn anyone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Troubleshooting entry for a failing local dev server still named .env.dev,
which is now the deployed DEV backend's file. Local development reads
.env.localhost.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three superseding waves on one branch left two kinds of residue.

The superseded plans still read as live agentic instructions — unchecked boxes,
"REQUIRED SUB-SKILL: implement this plan task-by-task", no supersession marker —
and their script blocks mandate `dev:local` -> `--mode dev`, precisely the
misnaming the final wave exists to fix. Mark all four as historical records.

DEVELOPMENT.md's Troubleshooting still credited the dev proxy with handling
self-signed certs, and CLAUDE.md's Service Layer still said "(proxied in dev)" —
both refuted by this branch's own correction: api.ts uses an absolute baseURL, so
the proxy never fires.

Also note in .env.example that REACT_APP_ENV is optional for localhost/dev, since
the documented `cp` procedure otherwise yields a badge the header doesn't imply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The banner exists to stop someone executing a stale plan, so its first sentence
should read cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@samutpra samutpra changed the title feat(env): add UAT target and rename modes to dev/prod feat(env): four env targets — localhost, dev, uat, prod Jul 15, 2026
@samutpra
samutpra merged commit acc7a12 into main Jul 15, 2026
5 checks passed
@samutpra
samutpra deleted the feat/env-modes branch July 15, 2026 07:26
samutpra added a commit that referenced this pull request Jul 16, 2026
commit 79843fa
Merge: 203c0cc 00a2a88
Author: Thammanoon Semapru <samutpra@users.noreply.github.com>
Date:   Thu Jul 16 15:34:20 2026 +0700

    Merge pull request #37 from CarmenSoftware-organization/fix/deploy-env-prod-filename

    fix(ci): write .env.prod (not .env.production) so the prod build finds its env

commit 00a2a88
Author: Thammanoon Semapru <samutpra@users.noreply.github.com>
Date:   Thu Jul 16 15:32:45 2026 +0700

    fix(ci): write .env.prod (not .env.production) so the prod build finds its env

    `build:prod` runs `vite build --mode prod`, which loads `.env.prod`. The deploy
    workflow wrote `.env.production` — a name Vite never loads for mode "prod" — so
    the file was ignored and the build failed the env guard:

        Error: [env] Missing REACT_APP_API_BASE_URL, REACT_APP_API_APP_ID for mode "prod".

    Left over from the env refactor (#33) that renamed .env.production -> .env.prod
    and switched to explicit `--mode prod`; the workflow and the deploy-gcs.sh
    comment still referenced the old name. Rename the written file to .env.prod and
    fix the stale comments.

    Verified locally: `bun run build:prod` succeeds with a .env.prod present.

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

commit 203c0cc
Author: Thammanoon Semapru <samutpra@users.noreply.github.com>
Date:   Thu Jul 16 15:27:36 2026 +0700

    chore: rename .env.uat to .env.production in GCS deployment workflow

commit 84b711e
Author: Thammanoon Semapru <samutpra@users.noreply.github.com>
Date:   Thu Jul 16 15:24:47 2026 +0700

    chore: rename generated .env.production to .env.uat in deployment workflow

commit c267613
Author: Thammanoon Semapru <samutpra@users.noreply.github.com>
Date:   Thu Jul 16 15:23:03 2026 +0700

    chore: remove GCP deployment workflow file
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant