Files
backend_fiesta/docs/ENVIRONMENT.md
Suriyakumarvijayanayagam 4474479735 Load .env.<APP_ENV>, validate config at boot, keep secrets out of the image
`main.go` only ever loaded `.env`; the `APP_ENV` switch described in
`.env.local` / `.env.production` did not exist, and a missing variable
surfaced one restart at a time as a log.Fatalf inside db.Connect.

config.Load now picks `.env.<APP_ENV>` (default local) then `.env`, with
real environment winning, reads every setting into one typed Config and
reports everything missing in one message. Production insists on a POS
signing secret; local warns when DB_HOST is not a local address. db,
redis and the image store take the Config instead of reading env
themselves.

Also:
- livehub read MQTT_USERNAME while everything else uses MQTT_USER, so the
  console stream connected to the broker unauthenticated. Both accepted.
- .dockerignore: `COPY . .` was baking .env.production into the image.
  Dockerfile sets APP_ENV=production.
- Drop utils/config.go (dead viper loader) and create_table.go (unused,
  hardcoded production DSN); go mod tidy removes viper.
- .env.example lists every variable the code reads; docs/ENVIRONMENT.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:04:33 +05:30

4.4 KiB

Environment & configuration

Everything the API reads from its environment goes through config/config.go. It loads the right .env file, reads every setting into one Config, and refuses to start — listing everything that is wrong in one message — before a single connection is attempted.

Which file loads

APP_ENV names the environment. It defaults to local.

Command Files loaded, in order
go run . .env.local, then .env
APP_ENV=production go run . .env.production, then .env
APP_ENV=staging go run . .env.staging, then .env

Precedence, highest first:

real environment  >  .env.<APP_ENV>  >  .env

A variable that is already set is never overwritten by a file, and no file has to exist. APP_ENV itself is read from the real environment before any file is opened — a file cannot decide which file gets loaded.

File Role
.env.example The complete list of settings, with comments. Start here.
.env.local The docker-compose stack. Every host is localhost.
.env.production The live hosts. Loaded only when asked for.
.env Shared base: fallbacks for whatever the file above left out. Keep it local.

Running locally

docker compose -f docker-compose.local.yml up -d   # postgres :5433, pgvector :5434, redis :6379
go run .                                           # APP_ENV unset → .env.local

An empty database is not enough — main.go runs migrations that assume the live schema. See init/README.md for loading a schema dump first.

Startup prints what it loaded and where it is pointed:

config: loaded .env.local
config: loaded .env
config: APP_ENV=local, listening on :1122, database nearle@localhost:5433/nearledb

If DB_HOST is not a local address under APP_ENV=local, it says so:

⚠️  APP_ENV=local but DB_HOST=66.116.x.x is not a local address — every write goes to that database for real

That is a warning, not a stop. There is no "local mode" that protects production: go run . against the live host creates real tenants and real logins, and runs schema migrations on boot.

Running in production

The container gets no .env file at all — .dockerignore keeps every .env* out of the image — and the Dockerfile sets APP_ENV=production. Every value comes from the platform's environment settings (Dokploy today; ConfigMaps/Secrets under Kubernetes).

Under APP_ENV=production startup additionally insists on:

  • POS_TOKEN_SECRET (or JWT_SECRET_KEY as a fallback), at least 16 characters.

A variable added to .env.production and not to the platform is a variable that is unset in production. Missing required ones stop the boot with the full list; missing optional ones (MQTT_URL, REDIS_HOST, USE_S3, CATALOGUE_DB_HOST) silently disable that subsystem — check the startup log lines when something is "not working".

What is required

Always Only when enabled
DB_HOST DB_USER DB_PASSWORD DB_NAME CATALOGUE_DB_HOST set → CATALOGUE_DB_USER/PASSWORD/NAME
(production) POS_TOKEN_SECRET USE_S3=true → S3_ENDPOINT/BUCKET/ACCESS_KEY/SECRET_KEY/REGION

A half-configured subsystem is an error, not a warning: a warning reads as "fine" in a log and turns into "why are there no images" a week later.

The committed credentials

The three .env files, including .env.production, are currently tracked in git (commit be47435), and an earlier .env was committed before 2026-08-03. Every credential in them has to be treated as public:

  1. Rotate the database, catalogue, Spaces, MQTT and Redis credentials and the POS signing secret, and update them in the platform.
  2. Take the files back out of the index and restore the ignore rules:
    git rm --cached .env .env.local .env.production
    printf '.env\n.env.*\n!.env.example\n' >> .gitignore
    
    The files stay on disk; they just stop being committed.