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 09:17:20 -05:00
parent 5f21d97178
commit 0a830cad1f
2 changed files with 29 additions and 6 deletions
@@ -56,7 +56,13 @@ const DEPLOYMENTS = [
},
{
file: 'docker-compose.prod.yml',
demoMode: 'false',
// TEMPORARY, and #191 restores it to 'false'. Production is in demo mode
// since 2026-08-25 — the stack was brought up during the cutover to this
// file before the live PayPal credentials were available, and demo mode is
// the sanctioned interim for that. This assertion is what keeps the state
// honest: it fails the moment the compose file and this expectation
// disagree, so neither can be changed quietly, in either direction.
demoMode: 'true',
// Deliberately unrestricted. Production has to be able to reach real
// customers, and it is the one environment where that is correct.
requiresMailAllowlist: false