# --------------------------------------------------------------------------- # Production runtime configuration. COMMITTED ON PURPOSE — carries no secret. # --------------------------------------------------------------------------- # # This file is the production environment. It is read by `next build` and, more # importantly, by the standalone `server.js` at boot (Next calls loadEnvConfig # on the server's working directory), so the deployed container knows the # platform host without anyone remembering to type it into a dashboard. # # ── Precedence, exactly as @next/env resolves it ──────────────────────────── # # 1. real process.env (Dokploy / docker -e / systemd) ← always wins # 2. .env.production.local # 3. .env.local ← LOCAL DEV ONLY. Never enters the image. # 4. .env.production # 5. .env ← this file, the floor everything falls back to # # A value already present in process.env is never overwritten by a file, so # setting LOYALY_API_BASE in Dokploy still overrides this — nothing here locks # the deployment in. It only removes "unset" as a possible state. # # ── Working on this locally? ──────────────────────────────────────────────── # Put your overrides in `.env.local` (gitignored, loaded ahead of this file). # Without one, `npm run dev` will talk to the PRODUCTION platform, because that # is what this file says. `.env.example` has the local values to copy. # The one shared Loyaly platform API (Behavision). Server-side only and # deliberately NOT NEXT_PUBLIC: publishing the host would let a browser bypass # the BFF, which is what keeps the access token out of JavaScript. # # NOT platform.loyaly.ai — that host serves THIS console, not the API. Pointing # the variable there makes the BFF call its own origin, which fails in a way # that looks like a broken login form rather than a misconfiguration. # apiClient.ts rejects that hostname by name for exactly this reason. # # NOT REQUIRED in production any more. Production accepts exactly one origin, so # an unset variable could never have meant another one, and platformApi resolves # it to that origin on its own. It stays here so `docker run` is self-describing # and so development has something to read. # # Why that change was needed: @next/env only fills a variable that is ABSENT. # Verified against the installed copy — a real environment variable set to the # EMPTY STRING stays empty and this file is NOT consulted. So one blank field in # a dashboard silently defeated the value below and took production down with # "LOYALY_API_BASE is required in production". LOYALY_API_BASE=https://mcp.loyaly.ai # Browser → this app's own BFF routes, which are same-origin. Empty is correct # and is what makes the console work on any hostname it is served from: # requests go to /api/... on whatever origin loaded the page (localhost:3100 in # dev, platform.loyaly.ai in production) and the server hop above reaches the # platform. Setting this to the platform host would send the browser straight # at the API with no session cookie and no token — do not. # # It is NEXT_PUBLIC, so it is inlined at BUILD time, not read at runtime. # Changing it in Dokploy's environment panel would do nothing without a rebuild. NEXT_PUBLIC_API_BASE= # AUTH_SECRET is deliberately NOT in this file. It is the ONLY variable this # deployment requires, and the only one that cannot ship. # # It signs the session cookie and encrypts the platform token bundle, so a # value committed here is a session-forging key in git — anyone who can read # the repo could mint a cookie for any user. It was already removed from the # Dockerfile once for that reason; do not reintroduce it here. # # Set it as a Dokploy environment variable in the RUNTIME panel — a value set as # a BUILD argument is not present when the server runs, which looks exactly like # never having set it. Alternatively mount the value and set AUTH_SECRET_FILE to # its path (the Docker/Swarm secret convention); AUTH_SECRET wins if both exist. # # Production refuses to sign sessions without it. Generate with: # # openssl rand -hex 32 # # Hex, not base64: a base64 value ends in '=' and can contain '+' and '/', and # an environment editor that splits a line on the first '=' can store that # truncated or empty. A silently-empty AUTH_SECRET looks exactly like an unset # one, which is a slow afternoon. Hex has nothing a parser can mangle.