feat(ops): put production in demo mode to complete the cutover (#191)

Production could not boot during the cutover to the committed compose file: `DEMO_MODE` is false there, which makes the three PayPal secrets required, and they were not available. Demo mode is the interim the compose file's own header sanctions for exactly this — the whole cart and checkout flow works and nobody is ever charged.

Two things made this cost more than it should have, and both are now written down rather than left to be rediscovered.

`DEMO_MODE` is hardcoded rather than interpolated, so setting a `DEMO_MODE` stack variable in Portainer does nothing at all — there is no `${...}` for it to substitute into and the file's value wins silently. That hardcoding is right: the one value deciding whether the shop takes money should not be flippable from a web UI without a commit anybody can read. But the failure mode reads as "I set it and it ignored me", so the line now says so.

Declaring `PAYPAL_CLIENT_ID=` with an empty value is identical to not declaring it. `isPresent` rejects a blank string deliberately, because set-to-nothing is a mistake rather than a value.

The state is loud in both places that can see it. The compose file leads with a banner saying production is taking no money, and `composeEnvironment.test.ts` asserts `DEMO_MODE` is `true` — that assertion is the guard, not a formality: it fails the moment the file and the expectation disagree, in either direction, so this cannot be switched back quietly and cannot be left on unnoticed.

#191 restores it.
This commit is contained in:
2026-08-26 08:38:05 -05:00
parent f5e3ec3e99
commit d79aad0adf
2 changed files with 29 additions and 6 deletions
+22 -5
View File
@@ -104,15 +104,32 @@ services:
- PGPASSWORD=${DB_PASSWORD}
- PGDATABASE=redefined
# ===================================================================
# PRODUCTION IS IN DEMO MODE. IT IS TAKING NO MONEY.
#
# The whole cart and checkout flow works end to end and NOBODY IS EVER
# CHARGED. This is the deliberate interim the note below describes, taken
# 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 #191. Set this back to `false`, fill the three stack
# variables below, and redeploy. 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.
#
# To bring the stack up before PayPal is configured, set this to `true`
# and the three PAYPAL_ lines can be removed. The full cart and checkout
# flow then works end to end and NOBODY IS EVER CHARGED. That is a
# deliberate interim state and a quiet disaster if it is left on.
- DEMO_MODE=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
- PAYPAL_ENV=live
- PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
- PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}