feat(ops): read DEMO_MODE from the stack rather than the compose file (#190)

Reverses the position the previous commit took. Hardcoding it made a value that gets flipped without a code change require a commit and a merge to flip, which is backwards — and it is the operator's call, not the file's.

No default, deliberately. `${DEMO_MODE:-false}` is the obvious form and the wrong one: a default decides whether the shop takes money on the operator's behalf, silently, whichever way it points. Having none is safe rather than fragile because `checkDemoMode` is strict — an unset stack variable substitutes to an empty string, and anything that is not exactly `true` or `false` refuses to boot naming DEMO_MODE. That strictness is the whole reason interpolating this one is defensible, so the line says so.

The compose guard had to learn the difference. It hands each deploying file's entries to the real validator, and a literal `${DEMO_MODE}` is not a value `checkDemoMode` accepts, so both the DEMO_MODE assertion and the validateEnv check failed the moment the file stopped holding a literal. Each deployment now declares the stack variables it supplies, and a bare `${VAR}` named there resolves to the declared value before the file is validated. Every other bare `${VAR}` stays opaque exactly as before — those are secrets, and what is checked of them is that the line exists.

Be clear about what that guard can prove. It cannot see Portainer, so it does not verify the stack actually holds `true`; nothing in this repository can. What it does is keep the intent beside the file and make the pair inseparable — hardcode the compose line and the registry disagrees, change the registry and it no longer describes the file. The runtime half is the boot check, which fails loudly rather than falling back. Verified by mutation: hardcoding `false` fails the DEMO_MODE assertion, and deleting the line fails that and `validateEnv`.

Restoring real payments is now two Portainer values and a redeploy, with no commit — which is what #190 asks for.
This commit is contained in:
2026-08-26 09:17:20 -05:00
parent 12b2f09d79
commit 6104ebb459
2 changed files with 62 additions and 18 deletions
+22 -12
View File
@@ -58,6 +58,12 @@
# Required stack environment variables — all secrets, all must be set in
# Portainer for this stack:
#
# DEMO_MODE `true` or `false`, exactly. Whether real payments are
# taken. Not a secret — it is here rather than written
# into the file below because it is the one value that
# gets flipped without a code change. There is no
# default: unset or mistyped refuses to boot rather than
# choosing for you.
# DB_PASSWORD Postgres password for the `redefined` database.
# SMTP_USER Brevo SMTP login.
# SMTP_PASSWORD
@@ -112,24 +118,28 @@ services:
# on 2026-08-25 to bring the stack up during the cutover to this file
# before the live PayPal credentials were to hand.
#
# Restoring it is #190. Set this back to `false`, fill the three stack
# variables below, and redeploy. Until then every order placed is a
# pretend one.
# Restoring it is #190: fill the three PayPal stack variables and set the
# DEMO_MODE stack variable to `false`. Both are Portainer values now, so
# restoring real payments needs no commit — which is the point, and also
# why this banner and the guard test exist to keep the state visible.
# Until then every order placed is a pretend one.
# ===================================================================
#
# Real payments. This is the difference between production and QA, and it
# is why the three PayPal secrets are required rather than optional — the
# app refuses to start without them when this is false.
#
# HARDCODED, AND NOT INTERPOLATED, ON PURPOSE. Portainer substitutes stack
# variables into this file; it does not hand them to the container. A
# `DEMO_MODE` stack variable therefore does NOTHING — there is no `${...}`
# here for it to substitute into, and the value below wins silently. That
# is deliberate: the one value that decides whether the shop takes money
# should not be flippable from a web UI without a commit anybody can read.
# It is also exactly what wasted an hour during the cutover, so it is
# written down rather than left to be rediscovered.
- DEMO_MODE=true
# Supplied by the stack, and with NO DEFAULT, deliberately.
# `${DEMO_MODE:-false}` is the obvious thing to write and the wrong thing:
# a default decides whether the shop takes money on the operator's behalf,
# silently, whichever way it points.
#
# Having no default is safe rather than fragile, because `checkDemoMode`
# is strict. An unset stack variable substitutes to an empty string, and
# anything that is not exactly `true` or `false` refuses to boot naming
# DEMO_MODE. So a missing or mistyped value fails loudly at startup
# instead of guessing — which is what makes interpolating this one safe.
- DEMO_MODE=${DEMO_MODE}
- PAYPAL_ENV=live
- PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
- PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}