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>
This commit is contained in:
109
.env.example
109
.env.example
@@ -1,30 +1,41 @@
|
||||
# Fiesta configuration.
|
||||
# Fiesta configuration — the complete list of settings, with local values.
|
||||
#
|
||||
# Copy to `.env` and fill in. `.env` and `.env.*` are gitignored (the one
|
||||
# exception is this file) — and they are gitignored for a reason: this
|
||||
# repository's history already contains a committed `.env` from before
|
||||
# 2026-08-03, so those database credentials are in the history and should be
|
||||
# rotated. Do not add another.
|
||||
# How the files are picked (config/config.go):
|
||||
#
|
||||
# `godotenv.Load()` in main.go reads `.env` from the working directory, so
|
||||
# `go run .` from this folder picks it up with no flags.
|
||||
# APP_ENV unset / local → .env.local then .env
|
||||
# APP_ENV=production → .env.production then .env
|
||||
#
|
||||
# A real environment variable always wins over a file, and no file has to
|
||||
# exist: on the deployed host the values come from the platform's environment
|
||||
# settings (Dokploy / Kubernetes), not from a file. Anything added here must be
|
||||
# added there too — a variable in this file and not in the platform is a
|
||||
# variable that is unset in production.
|
||||
#
|
||||
# Startup checks every required setting and prints everything that is missing
|
||||
# in one go, before any connection is attempted.
|
||||
#
|
||||
# The credentials in the committed .env.production have to be treated as
|
||||
# public: rotate them, and keep new values out of git.
|
||||
|
||||
# ── Environment ─────────────────────────────────────────────────────────────
|
||||
# local | production. Production requires POS_TOKEN_SECRET and never falls
|
||||
# back to localhost defaults. Set in the real environment, not in a file: a
|
||||
# file cannot decide which file gets loaded.
|
||||
#APP_ENV=local
|
||||
|
||||
# ── Where it listens ────────────────────────────────────────────────────────
|
||||
# 1122 is what production serves on. Change it locally to run a second copy
|
||||
# beside something else; the console then points at the same number.
|
||||
# 1122 locally; production serves on 1009 (matching the Dockerfile's EXPOSE).
|
||||
APP_PORT=1122
|
||||
|
||||
ENV=development
|
||||
|
||||
# ── The main database (nearledb) ────────────────────────────────────────────
|
||||
#
|
||||
# ⚠️ POINTING THIS AT PRODUCTION MAKES LOCAL TESTING WRITE TO PRODUCTION.
|
||||
#
|
||||
# There is no "local mode" that protects you: `go run .` against the live host
|
||||
# creates real tenants, real logins and real stock movements, and main.go runs
|
||||
# schema migrations on boot. If the point of running locally is to try a change
|
||||
# before it is deployed, a local Postgres with a dump restored into it is the
|
||||
# only version that actually does that.
|
||||
# schema migrations on boot. Startup warns when APP_ENV=local and DB_HOST is
|
||||
# not a local address, but it does not stop you.
|
||||
#
|
||||
# These match docker-compose.local.yml, so `docker compose -f
|
||||
# docker-compose.local.yml up -d` and `go run .` work together with no edits.
|
||||
DB_HOST=localhost
|
||||
@@ -36,10 +47,9 @@ DB_PASSWORD=localdev
|
||||
# ── The catalogue database (pgvector) ───────────────────────────────────────
|
||||
#
|
||||
# A separate connection on purpose, so catalogue work never touches nearledb.
|
||||
# Leave blank to start without it: catalogue endpoints then fail at query time
|
||||
# rather than at boot, which is fine for testing anything else.
|
||||
# 5434, not 5432: a developer machine usually has something on 5432 already,
|
||||
# and a silent connection to the wrong database is worse than a refused one.
|
||||
# Leave CATALOGUE_DB_HOST blank to start without it: catalogue endpoints then
|
||||
# fail at query time rather than at boot. With a host set, the other four are
|
||||
# required. 5434, not 5432: a developer machine usually has something on 5432.
|
||||
CATALOGUE_DB_HOST=localhost
|
||||
CATALOGUE_DB_PORT=5434
|
||||
CATALOGUE_DB_NAME=cataloguedb
|
||||
@@ -48,12 +58,65 @@ CATALOGUE_DB_PASSWORD=localdev
|
||||
|
||||
# ── Redis — POS terminal presence, under a TTL ──────────────────────────────
|
||||
#
|
||||
# Optional. Losing the health board is an inconvenience; losing a sale is not,
|
||||
# so the API runs without it.
|
||||
# Optional: leave REDIS_HOST blank to run without it. Losing the health board
|
||||
# is an inconvenience; losing a sale is not, so the API runs without it.
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_USER=
|
||||
REDIS_USER=default
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# ── DigitalOcean Spaces (S3-compatible) — catalogue product images ──────────
|
||||
#
|
||||
# Optional locally. With USE_S3=true every S3_* value below is required.
|
||||
USE_S3=false
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_KEY=
|
||||
S3_ENDPOINT=
|
||||
S3_BUCKET=
|
||||
S3_REGION=
|
||||
|
||||
# ── POS terminals — the MQTT broker the in-store tills publish to ───────────
|
||||
#
|
||||
# Optional locally: with MQTT_URL blank the ingest and the console live stream
|
||||
# stay quiet and the HTTP endpoints still work. In production a blank MQTT_URL
|
||||
# means every till queues its bills silently — on startup you should see
|
||||
# three lines reading "pos: subscribed to nearle/pos/+/+/...".
|
||||
MQTT_URL=
|
||||
MQTT_USER=
|
||||
MQTT_PASSWORD=
|
||||
# Unique per replica: a second connection with the same id evicts the first.
|
||||
# Defaults to HOSTNAME (the pod name) when unset.
|
||||
MQTT_CLIENT_ID=
|
||||
# always | never — otherwise a StatefulSet pod ending in "-0" is elected and
|
||||
# anything else consumes. See messaging/posmqtt.go.
|
||||
#POS_MQTT_CONSUMER=
|
||||
|
||||
# ── Auth ────────────────────────────────────────────────────────────────────
|
||||
# Signs POS terminal sessions; at least 16 characters. Falls back to
|
||||
# JWT_SECRET_KEY when unset. Required in production.
|
||||
POS_TOKEN_SECRET=local-dev-signing-secret-not-real
|
||||
JWT_SECRET_KEY=
|
||||
USER_CONTEXT_KEY=
|
||||
USER_CONTEXT_KEY=nearle
|
||||
# true to make the POS routes require a terminal session.
|
||||
#POS_AUTH_REQUIRED=false
|
||||
|
||||
# ── Scan-to-order — the embedding model behind product recognition ─────────
|
||||
#
|
||||
# MUST be the model that filled the catalogue's `embedding` column: vectors
|
||||
# from two models are not comparable and pgvector will rank garbage without
|
||||
# complaint. The first search reads the column's width and refuses a mismatch
|
||||
# with an error that names both numbers.
|
||||
# Optional: with no provider the search matches on words alone (works, ranks
|
||||
# worse). openai = any OpenAI-compatible /embeddings endpoint (set
|
||||
# EMBEDDING_BASE_URL for Azure, Ollama, vLLM...); gemini = Google AI Studio.
|
||||
EMBEDDING_PROVIDER=
|
||||
EMBEDDING_MODEL=
|
||||
EMBEDDING_API_KEY=
|
||||
EMBEDDING_BASE_URL=
|
||||
# 0 = the model's default width.
|
||||
EMBEDDING_DIMENSIONS=0
|
||||
|
||||
# ── Geocoding ───────────────────────────────────────────────────────────────
|
||||
# Google Geocoding when set; OpenStreetMap's Nominatim otherwise.
|
||||
GEOCODER_API_KEY=
|
||||
|
||||
Reference in New Issue
Block a user