-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path.env.example
More file actions
424 lines (412 loc) · 23.9 KB
/
Copy path.env.example
File metadata and controls
424 lines (412 loc) · 23.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
# Copy to .env and fill in secrets
OPENAI_API_KEY=
# Password for the one Postgres every service shares. Compose defaults it to
# `cloudsforge` if you leave it unset, so a deployment that skips this line gets
# that — set it before the FIRST boot. Changing it afterwards does NOT change
# the password of an already-initialised pgdata volume: you must also run
# ALTER USER cloudsforge WITH PASSWORD '...';
# against the running database, or every service loses its connection at once.
# Postgres is published on 127.0.0.1 only, so this is not reachable off the box.
POSTGRES_PASSWORD=cloudsforge
NIMBUS_DATABASE_URL=postgres://cloudsforge:cloudsforge@localhost:5432/nimbus
GAME_DATABASE_URL=postgres://cloudsforge:cloudsforge@localhost:5432/game
PAY_DATABASE_URL=postgres://cloudsforge:cloudsforge@localhost:5432/pay
NIMBUS_PORT=4001
# Encrypts Nimbus's RS256 signing key at rest. REQUIRED, min 24 chars,
# placeholders refused — Nimbus does not boot without it and will not fall back
# to storing the key in the clear. Generate: openssl rand -hex 32
#
# That key is the estate's universal forging credential: every service checks
# `iss` plus `aud: cloudsforge` and nothing else, so a token minted with it is
# admin in Pay, the game, Crucible, ForgeMint and ForgeKeyvault alike. It used to
# be plaintext JSONB in a database every service shares one Postgres role on.
# Keep this string OUT of that database and out of any backup of it — the
# separation is the whole mechanism. Same rule, same reason, as
# KEYVAULT_MASTER_SECRET below.
NIMBUS_KEY_SECRET=
# Baked into every JWT and checked by every service. COMMENTED for the same
# reason as the lists below: the compose default is already this exact value,
# so setting it here only pins localhost onto a public deployment — see the
# PUBLIC DEPLOYMENT block at the bottom, where you set the real one.
# NIMBUS_ISSUER=http://localhost:4001
# The origin Nimbus builds a password-reset link from — the one URL it produces
# that is read somewhere other than the browser that asked for it, so it comes
# from here and never from the request's Host header (which let a stranger point
# a genuine reset email at a host they controlled). COMMENTED for the same
# reason as NIMBUS_ISSUER: compose already defaults it to
# https://account.${CLOUDSFORGE_APEX}, and pinning localhost here would put a
# localhost link in every reset email a public deployment sends.
# NIMBUS_PUBLIC_URL=http://localhost:4001
# Origins the account portal may hand a sign-in code back to. Explicit list of
# real app surfaces only — never a wildcard. Left COMMENTED on purpose: the
# compose default already covers localhost plus the CLOUDSFORGE_APEX subdomains,
# and setting it here to the localhost-only list would silently undo that.
# PORTAL_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3003,http://localhost:4004,http://localhost:4006,http://localhost:4010
# Peers allowed to set X-Forwarded-*. Default: loopback,linklocal,uniquelocal.
# TRUST_PROXY=loopback
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=
ADMIN_HANDLE=admin
# ─── Outbound mail (Nimbus) — password reset links ───────────────────────────
#
# The only mail this estate sends. Set it and a user who clicks "forgot
# password" is emailed a single-use link that expires in 30 minutes. Leave
# SMTP_HOST unset and nothing is sent — the request is still recorded and an
# operator issues the link from the admin console (Users → Reset password),
# which is how it worked before this existed and is a supported way to run.
# Nimbus says which of the two it is, at info, on every boot.
#
# There is no provider code and no SDK: this is plain SMTP, so Brevo, Resend,
# SendGrid, Mailtrap, a Gmail app password and your own postfix are all the same
# four settings. Changing provider is an edit here and a restart.
#
# FREE CREDENTIALS IN TWO MINUTES (Brevo, 300 emails/day, no custom domain and
# no card):
# 1. Sign up at https://www.brevo.com and confirm the address.
# 2. SMTP & API → SMTP → "Generate a new SMTP key".
# 3. SMTP_HOST=smtp-relay.brevo.com, SMTP_PORT=587,
# SMTP_USER=<the login shown on that page, e.g. 8a1b2c001@smtp-brevo.com>,
# SMTP_PASS=<the generated key, NOT your account password>.
# 4. SMTP_FROM must be an address Brevo has verified — the one you signed up
# with works immediately under Senders & IP. A custom domain is optional.
# Any other provider works with the same four variables; only the host and the
# credentials change.
#
# Unset = no mail. Everything else is only read when this is set.
SMTP_HOST=
# 587 is STARTTLS and is what every provider above documents first. 465 is
# implicit TLS and additionally needs SMTP_SECURE=true; the two are NOT
# interchangeable, and 465 with secure unset hangs until the connect timeout.
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=
# Treat as a secret: it can send mail as you, and nothing rate-limits that but
# the provider. Never your account password — providers issue a separate key.
SMTP_PASS=
# The From header, as a full RFC 5322 address. Must be an address the provider
# has authorised for you, or it accepts the connection and rejects the message.
SMTP_FROM=CloudsForge <no-reply@example.com>
# Optional. Where a reply goes, if you would rather it was not the From address.
SMTP_REPLY_TO=
GAME_PORT=4002
NIMBUS_JWKS_URL=http://localhost:4001/.well-known/jwks.json
PAY_PORT=4003
# There is no payment provider and no invoice here. Shards are funded by
# on-chain deposit and conversion only — see the deposit watcher below. The
# provider path was deleted rather than configured: its `mock` setting settled
# any invoice for free, and a crypto-only ecosystem never needed the rest of it.
# Shared secret peer services present on Pay's /internal routes to move Shards
# WITHOUT a user's bearer token — a settlement loop has no live token to forward.
# Crucible needs it to charge its performance fee; leave it unset and /internal
# answers 401 to everything, which surfaces in the CALLING service's logs.
# REQUIRED for Crucible, min 24 chars. Generate: openssl rand -hex 24
#
# It is the ONLY thing standing between a caller and reading any balance,
# debiting any user, crediting any user or liquidating any user's custodied
# coin — the routes take a userId as a parameter. Treat it as a master key.
# /internal is kept off the internet by two locks that are not this one: 4003
# is bound to 127.0.0.1 in compose, and the tunnel returns 404 for /internal/*.
PAY_SERVICE_TOKEN=
# Forge Pay multi-chain deposit watcher — SEE incoming payments across chains.
# Default network for validation; switch to 'mainnet' for real funds.
PAY_DEPOSIT_NETWORK=testnet
PAY_DEPOSIT_POLL_SECONDS=30
# Coin -> Shard price oracle. Median of CoinGecko/Coinbase/Kraken/Binance, refreshed on a
# timer, never in the request path. A quote older than MAX_AGE is refused (503), not used.
PAY_ORACLE_REFRESH_SECONDS=60
PAY_ORACLE_MAX_AGE_SECONDS=900
PAY_ORACLE_MIN_SOURCES=2
PAY_ORACLE_MAX_DIVERGENCE_BPS=500
PAY_ORACLE_TIMEOUT_MS=5000
# Charged against the user so the platform is not exposed to price movement between
# quote and settlement. 200 = 2%.
PAY_CONVERSION_SPREAD_BPS=200
# EMBER has no exchange, so there is no price to fetch — it is an ADMINISTERED rate.
# It is NO LONGER an environment variable: the price lives in a table seeded by pay's
# migrate() and is edited at runtime through PUT /admin/prices/EMBER, which records who
# set it and when. A price set here would be silently ignored.
#
# The rest of Forge Pay's knobs have safe defaults and are documented at
# repos/forge-pay/services/pay/src/env.ts. Two are worth knowing, and they
# interact badly:
# PAY_WITHDRAWALS_ENABLED default TRUE — withdrawals are accepted out of the box
# PAY_SWEEP_ENABLED default false — the sweeper that funds the treasury
# opens no new sweeps. This is a DECISION, not a
# limitation: keyvault has had a signable `sweep`
# shape since it grew one, and its destination is a
# treasury an operator pinned inside the vault
# (forge-keyvault/src/signing.ts EvmShape,
# routes/vault.ts SWEEPABLE_FAMILIES).
#
# So on a fresh deployment a withdrawal is ACCEPTED, sits `pending` while pay
# logs "the treasury cannot cover it" every 20s, and is then automatically
# REFUNDED once PAY_WITHDRAWAL_STUCK_MINUTES passes (default 60) — safe to do,
# because nothing was ever signed. Not limbo, but an hour of a user's time spent
# on an answer we could have given immediately.
#
# Turning sweeping on takes three things, and only the last two are env vars:
#
# 1. Pin a treasury, per chain and network, through ForgeKeyvault's Nimbus
# admin API — POST /admin/treasuries/:chain/:network/mint to create the
# address, then PUT /admin/treasuries/:chain/:network to pin it, in that
# order. Nothing sweeps to an unpinned candidate. This is an admin
# credential and a deliberate act, not a value in this file.
# 2. PAY_SWEEP_ENABLED=true.
# 3. Optionally PAY_TREASURY_TARGET_<COIN> (e.g. PAY_TREASURY_TARGET_EMBER=250)
# to hold a float. It defaults to zero, which makes the sweeper purely
# demand-driven — it moves what queued withdrawals actually need, nothing
# more.
#
# Decide (3) on custody grounds rather than convenience. A withdrawal must be
# payable to any address a user names, so the treasury's destination is
# unconstrained by design: every coin in the treasury is inside the blast radius
# of the KEYVAULT_SERVICE_TOKEN, and every coin left in a deposit address is
# outside it. A float buys immediacy on the first withdrawal after a quiet
# period — otherwise a confirmation depth of latency, ~45s on XRP, about a
# quarter of an hour on EMBER — and pays for it in standing exposure.
#
# Sweeping is available for the EVM, Ember and XRP families only. Bitcoin and
# Solana deposit addresses are still refused before decryption, because neither
# has an output policy built yet — so on those two chains the treasury really
# must be funded by hand. The full argument is DEPOSIT-SWEEP.md and the
# authoritative commentary is repos/forge-pay/services/pay/src/env.ts.
# Hearth local testnet coin (EMBER) — the seed node's ETHEREUM JSON-RPC, not its
# REST port (compose: http://hearth-testnet-seed:8545). Pay speaks eth_* here.
# Pointed at 8645 it gets a 200 carrying the legacy getinfo shape, which every
# client reports as an empty chain rather than as a wrong URL.
HEARTH_TESTNET_RPC=http://localhost:8545
HEARTH_MAINNET_RPC=
# Bitcoin (Esplora/Blockstream REST)
BTC_TESTNET_API=https://blockstream.info/testnet/api
BTC_MAINNET_API=https://blockstream.info/api
# Ethereum (JSON-RPC)
ETH_TESTNET_RPC=https://ethereum-sepolia-rpc.publicnode.com
ETH_MAINNET_RPC=https://ethereum-rpc.publicnode.com
# Solana (cluster RPC)
SOL_TESTNET_RPC=https://api.devnet.solana.com
SOL_MAINNET_RPC=https://api.mainnet-beta.solana.com
# XRP Ledger (rippled JSON-RPC)
XRP_TESTNET_RPC=https://s.altnet.rippletest.net:51234
XRP_MAINNET_RPC=https://s1.ripple.com:51234
# ForgeMint — cross-chain token generator
FORGE_MINT_PORT=4004
FORGE_MINT_DATABASE_URL=postgres://cloudsforge:cloudsforge@localhost:5432/forgemint
PAY_API_URL=http://localhost:4003
KEYVAULT_URL=http://localhost:4005
# Shared secret for forge-mint -> keyvault server-to-server calls.
# REQUIRED, min 24 chars. Generate: openssl rand -hex 24
KEYVAULT_SERVICE_TOKEN=
# RPC endpoints for the chains ForgeMint deploys to. Each chain in
# SUPPORTED_CHAINS ships a working public default for both of its networks, so
# every one of these is optional — set one only to replace the public node for
# that exact chain and network, which is usually because the public node is
# rate-limiting you.
#
# The variable is per chain AND per network: RPC_<CHAINID>_<NETWORK>, chain ids
# ethereum | bsc | polygon | arbitrum | base | solana, network mainnet |
# testnet. There is deliberately no single EVM_RPC_URL — one URL cannot serve
# five different EVM chains, and a shared value would point a Polygon deploy at
# an Ethereum endpoint. (Two such variables did exist here until CF-43. They
# were never read: the fallback that consulted them only fired when a chain had
# no baked default, which none does.)
#
# These are usually keyed provider URLs with the credential in the path
# (Infura /v3/<key>, Alchemy /v2/<key>). forge-mint never logs one in full —
# `safeUrl` in services/forge-mint/src/obs.ts keeps the origin and the first
# path segment and drops the rest — but treat the value as a secret in .env.
# RPC_ETHEREUM_MAINNET=https://mainnet.infura.io/v3/<key>
# RPC_ETHEREUM_TESTNET=https://sepolia.infura.io/v3/<key>
# RPC_SOLANA_MAINNET=https://<name>.solana-mainnet.quiknode.pro/<key>/
# ForgeKeyvault — per-address key custody
KEYVAULT_PORT=4005
KEYVAULT_DATABASE_URL=postgres://cloudsforge:cloudsforge@localhost:5432/keyvault
# Master secret used to derive per-address AES-256-GCM data keys.
# REQUIRED, min 24 chars. Generate: openssl rand -hex 32
# WARNING: there is no key rotation — changing this after addresses exist makes
# every already-custodied private key permanently undecryptable.
KEYVAULT_MASTER_SECRET=
# Where per-address encrypted keys live (mounted permanent volume in Docker)
KEYVAULT_DATA_DIR=./.keyvault-data
# Optional: mount /var/run/docker.sock to provision a container+volume per address
KEYVAULT_DOCKER_SOCKET=/var/run/docker.sock
# Crucible — algorithmic trading (http://localhost:4006)
# Serves both its SPA and its API on one port, like ForgeMint.
CRUCIBLE_PORT=4006
CRUCIBLE_DATABASE_URL=postgres://cloudsforge:cloudsforge@localhost:5432/crucible
# LIVE BOTS MOVE REAL MONEY. A live bot buys and sells the coin Pay custodies for
# a user and is charged a share of its gains, so this is off unless you mean it.
# Backtesting and paper trading are unaffected and always work.
CRUCIBLE_LIVE_ENABLED=false
# Share of a live bot's gains above its high-water mark. 1500 = 15%. Frozen onto
# each bot at creation, so changing this never re-prices a bot already running.
CRUCIBLE_PERFORMANCE_FEE_BPS=1500
# How often every running bot is evaluated, and how often fees are settled.
CRUCIBLE_TICK_SECONDS=60
CRUCIBLE_SETTLE_SECONDS=3600
# Public candle feeds. Binance is primary (deepest history, 1000 bars a call);
# Kraken is the fallback, because Binance answers a restricted region with a 451
# on every request forever rather than something a retry fixes.
CRUCIBLE_BINANCE_API=https://api.binance.com
CRUCIBLE_KRAKEN_API=https://api.kraken.com
# Lantern — platform logs and grouped errors (http://localhost:4010)
# Sign in with any CloudsForge account holding the `admin` role; it appears in
# the product switcher on every site for those users.
#
# LANTERN_TOKEN is the break-glass way in, used as ?token=… when SSO cannot
# work — which includes the case you will care about most, Nimbus being the
# thing that is broken. Set it. Generate: openssl rand -hex 32
LANTERN_TOKEN=
# Days of raw log lines to keep. Grouped issues are kept for 90 regardless,
# since they are small and are the thing you actually read.
LANTERN_RETENTION_DAYS=7
# Which socket peers may name the client with cf-connecting-ip or
# x-forwarded-for. /ingest/client is quota'd per client address, and behind the
# cloudflared tunnel every public request has the same socket peer — so without
# this the quota is one bucket of 120/min shared by the whole internet, and
# anyone can hold it empty. The default covers the connector (loopback, or the
# compose bridge) and trusts nothing that arrives from a public address.
# Keywords `loopback`, `private` and `all`, literal addresses, IPv4 CIDR.
# LANTERN_TRUSTED_PROXIES=loopback,private
# Raise to `debug` on any service to turn detail up without a code change.
# Applies to every service and to Lantern itself.
LOG_LEVEL=info
# Beacon — status page, probes and functional journeys (http://localhost:4011)
#
# Lantern reads what already went wrong. Beacon provokes what should go right:
# it probes every service on a 30-second cycle and drives real scenarios —
# sign in, hand off between products, credit and debit a wallet, place a
# backtest, read the chain — against the running stack on a schedule.
#
# It needs no configuration to be useful. Probes and the chain checks run out
# of the box. Everything below unlocks a further set of journeys, and every one
# of them reports `skip` with its reason when unset rather than `fail`, because
# a monitor that shows red for missing config is a monitor you learn to ignore.
#
# Same sign-in as Lantern, and the same break-glass. Generate: openssl rand -hex 32
BEACON_TOKEN=
#
# A long-lived synthetic account, used by every journey that must be somebody.
# Register it once through the normal sign-up page and put the credentials
# here. Beacon reuses it rather than registering per run, because Nimbus has no
# account deletion — no DELETE route and no DELETE method in its CORS — so a
# journey that registers every five minutes leaves 105,000 permanent rows a
# year. Registration is still tested, hourly, by `identity.signup`, which
# namespaces its addresses so they can be removed:
# DELETE FROM users WHERE email LIKE 'beacon+%';
BEACON_SYNTHETIC_EMAIL=
BEACON_SYNTHETIC_PASSWORD=
BEACON_SYNTHETIC_HANDLE=beacon
# Ceiling on the value a money journey may move, in Shards. These are minted
# and burned against the synthetic account through Forge Pay's /internal
# surface, so nothing real is spent — but a bug in a journey should cost a
# rounding error rather than a treasury. Requires PAY_SERVICE_TOKEN, set above.
BEACON_SYNTHETIC_SHARDS=100
#
# Chromium drives the four static front-ends. Without it, a bundle that 404s or
# a SPA that throws on boot is invisible: nginx answers `GET /` with a 200
# either way. Costs roughly 150MB of image. Set false on a small box and the
# browser journeys skip and say so.
BEACON_BROWSER_ENABLED=true
#
# Publish the status view without a sign-in. It carries service names, up/down
# and uptime percentages and nothing else — no error text, no journey step
# names, no latency, no chain internals — because all of those describe the
# shape of an outage to whoever is causing it.
BEACON_PUBLIC_STATUS=false
#
# Does this deployment expect to produce blocks itself?
#
# Leave true while anything here mines. Set false on a seed or archive
# deployment that only holds the chain and serves it to peers: a node with no
# local hashrate is not broken, but a stall check cannot tell that apart from a
# chain that has died, so hearth.liveness and hearth.mempool would sit
# permanently red. An alert nobody believes is worse than no alert, because it
# discredits the ones next to it. Height is still recorded and still graphed —
# it simply stops being an outage that an external miner has not shown up.
BEACON_CHAIN_EXPECT_BLOCKS=true
#
# The EIP-155 chain id every Hearth node here must report over JSON-RPC, in hex
# from eth_chainId and in decimal from net_version.
#
# 7412 is hearth-testnet, 7411 is hearth mainnet, and this is the number that
# stops a transaction signed for one from being valid on the other. A node
# refuses to start on a network it does not recognise, so the drift worth
# monitoring is the one that passes its own validation: HEARTH_CHAIN_ID set
# explicitly to something valid but wrong. Nothing then misroutes and no
# balance reads incorrectly — the loss is silent. Every testnet transaction
# becomes replayable on mainnet, and ForgeKeyvault, which resolves the expected
# id independently, begins refusing withdrawals with 403 binding_mismatch from
# a service that cannot explain why.
#
# Change this only when you change what the nodes are. It is pinned rather than
# read back from the node on purpose: a monitor that adopts the value it is
# meant to be checking asserts nothing at all.
#
# It defaults to HEARTH_CHAIN_ID below, so there is ONE number to change.
BEACON_HEARTH_CHAIN_ID=7412
#
# The same id, for the browser wallet and the block explorer (`hearth-web`).
#
# It is separate from the node's own HEARTH_NETWORK because the bundle is a set
# of static files with no build step: nginx templates this into a
# `<meta name="hearth-chain-id">` at boot (repos/hearth/web/nginx.conf), and
# repos/hearth/web/assets/chain.js reads it once for the whole page. The wallet
# then binds every EIP-155 signature to it.
#
# Deliberately NOT taken from whatever eth_chainId the node reports: the wallet
# accepts a `?rpc=<url>` override, so a node that got to choose the chain id
# would get to choose what its visitor's signature is valid on. The reported id
# is compared against this one and the mismatch is shown; it is never adopted.
#
# BEACON_HEARTH_CHAIN_ID falls back to this, so setting it here moves the
# monitor and the wallet together — the drift this replaced was a wallet
# signing for 7411 while every node in the file ran 7412.
HEARTH_CHAIN_ID=7412
#
# One webhook, fired on incident open and close and on nothing else. Plain
# JSON with a `text` field, so Slack, Discord and a shell script all work.
# An alert per failed probe is an alert nobody reads.
BEACON_WEBHOOK_URL=
# Public base URL of Beacon itself, so an alert can link back to the incident.
BEACON_SELF_URL=
# ─────────────────────────────────────────────────────────────────────────────
# PUBLIC DEPLOYMENT — leave every one of these unset for local development.
#
# Set CLOUDSFORGE_APEX and the origin allowlists follow. The compose defaults
# for CORS_ORIGINS and PORTAL_ALLOWED_ORIGINS are built from it, and already
# carry the localhost origins too, so this one line is enough to make a public
# deployment work — you do not need to write either list out by hand.
# CLOUDSFORGE_APEX=cloudsforge.online
#
# NIMBUS_ISSUER is the exception: it is baked into every JWT and checked by
# every service, so it is a single exact URL, not a list, and leaving it at the
# localhost default issues real users tokens whose issuer nothing on the
# internet can reach.
# NIMBUS_ISSUER=https://nimbus.cloudsforge.online
#
# NIMBUS_PUBLIC_URL follows the apex too (compose defaults it to
# https://account.${CLOUDSFORGE_APEX}), so set it only if the account portal is
# reached on some other hostname. It is the origin of every password-reset link
# this estate sends, so a wrong value sends users somewhere that does not answer
# — and it is deliberately not taken from the request, because that let anyone
# who knew an address have this deployment mail them a link to a host of their
# choosing.
# NIMBUS_PUBLIC_URL=https://account.cloudsforge.online
#
# Only set the lists below to REPLACE the derived defaults (they win outright —
# the apex-derived and localhost origins are then dropped). Both are explicit
# lists, never a wildcard: for the hand-back list any one compromised subdomain
# would otherwise be able to receive a sign-in hand-off.
# CORS_ORIGINS=https://cloudsforge.online,https://play.cloudsforge.online,https://admin.cloudsforge.online,https://hearth.cloudsforge.online,https://mint.cloudsforge.online,https://crucible.cloudsforge.online,https://lantern.cloudsforge.online,https://account.cloudsforge.online
# PORTAL_ALLOWED_ORIGINS=https://cloudsforge.online,https://play.cloudsforge.online,https://admin.cloudsforge.online,https://hearth.cloudsforge.online,https://mint.cloudsforge.online,https://crucible.cloudsforge.online,https://lantern.cloudsforge.online
#
# Pin the images to a release rather than tracking main.
#
# This ONE value tags fourteen images built by seven different repositories, so
# it can only ever be a version they all publish: tag vX.Y.Z in each product
# repo and every image gets X.Y.Z (docker/metadata-action's {{version}} drops
# the leading v). A commit sha cannot work here — sha-<commit> exists for the
# single repo that commit belongs to and nowhere else, and the other thirteen
# pulls fail with a 403 that reads like a permissions problem. See the README,
# "Pinning a release".
# CLOUDSFORGE_TAG=0.3.0