Files
website/server/.env.example
Claude c4ab8b9b9d
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 26s
PR Checks / client-build (pull_request) Successful in 29s
PR Checks / server-tests (pull_request) Successful in 2m32s
docs(email): SMTP setup, the three postures, and the upgrade note
The operator-facing half of engagement Phase 1. README's stack table and
security section, plus both .env.example files, all pointed at the
removed Connect Gmail flow.

The env comments now name the three supported postures rather than one
provider — a relay as the recommendation, smtp.gmail.com:587 with an app
password as the shortest migration, an unauthenticated local MTA as the
third — and point at docs/website/UPGRADE_NOTES.md for the deployment
this actually happens to.

The OpenAPI spec is regenerated: two routes gone, three annotations
rewritten, and the dashboard's new warnings[] documented.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 20:54:04 -05:00

169 lines
9.1 KiB
Plaintext

# ─── Runic Gateway server — local dev environment ───
# Copy to server/.env for running `npm run dev` outside Docker.
# (In Docker, the root .env / docker-compose provides these instead.)
NODE_ENV=development
PORT=3000
# Separate, unpublished port for server<->bot internal traffic (the decrypted
# bot-token route). Must match the port in bot/.env's SITE_INTERNAL_URL and must
# never be exposed through a public reverse proxy. See issue #33.
INTERNAL_PORT=3001
# Logging — written to BOTH the console and a log file (default <server>/logs/app.log).
LOG_LEVEL=debug # console verbosity: error | warn | info | debug
FILE_LOG_LEVEL=debug # file verbosity
LOG_TO_FILE=true # set false for console-only
# LOG_DIR= # defaults to server/logs
# LOG_FILE=app.log
# Point at a local or Dockerized MariaDB
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=runic_gateway
DB_USER=runic
DB_PASSWORD=change-me-db-password
JWT_SECRET=dev-only-change-me
JWT_EXPIRES_IN=1d
COOKIE_SECURE=auto
COOKIE_NAME=rg_token
# Trusted-device MFA ("Trust this device"). The trust cookie's name, how long a
# device stays trusted (skips the TOTP step, never the password), the per-user cap
# (no silent pruning — an over-cap trust is refused), and how many single-use
# recovery codes are generated at 2FA enrollment.
TRUST_COOKIE_NAME=rg_trust
TRUSTED_DEVICE_TTL_DAYS=30
MAX_TRUSTED_DEVICES=10
RECOVERY_CODE_COUNT=10
# Encryption key for secrets stored at rest (OAuth client secrets in auth_providers).
# Any string — hashed to a 256-bit AES-GCM key. REQUIRED in production; in dev an
# insecure key is derived from JWT_SECRET if unset (with a warning).
SECRET_ENC_KEY=dev-only-change-me-too
# Public base URL of this app, used to build the OAuth redirect_uri
# (${APP_BASE_URL}/api/v1/auth/sso/:provider/callback). Set this in production so
# the callback URL matches what you register with Google/Discord. If unset, it is
# derived from the incoming request (fine for local dev).
APP_BASE_URL=http://localhost:5173
# Short-lived mobile access token lifetime + refresh token lifetime (Part 2).
MOBILE_ACCESS_TTL=15m
MOBILE_REFRESH_TTL_DAYS=30
# Reverse-proxy trust. Request path: client -> Pangolin -> newt agent "ptero"
# (separate VM) -> this app. ptero is the hop that connects to us, so pin
# TRUST_PROXY to ptero's LAN IP: Express then honours X-Forwarded-For ONLY on
# connections from ptero, and req.ip / req.secure reflect the real client (used
# by rate limiting, backoff, bot-ban, activity log).
# <ptero LAN IP> -> e.g. 10.0.0.42 (RECOMMENDED in prod; requires a static
# DHCP reservation for ptero in Omada — a lease change would
# silently break IP trust)
# an integer -> that many hops (fallback if you can't pin an IP)
# false -> no proxy (direct connections)
# NOTE: a blanket "true" is intentionally rejected (coerced to 1) — it would let
# clients spoof their IP via a forged X-Forwarded-For and dodge rate limits/bans.
TRUST_PROXY=1
# Set to 1 to log each request's raw peer address + X-Forwarded-For + resolved
# req.ip, so you can verify/refresh ptero's IP without redeploying. Noisy —
# leave off in normal operation.
DEBUG_TRUST_PROXY=0
# Optional TOTP two-factor (opt-in per user).
# TOTP_ISSUER defaults to BRAND_NAME; BRAND_* live in the root .env (see root .env.example)
TOTP_ISSUER=Runic Gateway
# How long the "password verified, awaiting code" step stays valid.
TOTP_CHALLENGE_TTL=5m
# Created on first boot if the users table is empty
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-admin-password
# Email is configured in Admin → Settings → Email, not here: pick a mail
# transport (SMTP) and enter its host, port and credentials, stored encrypted in
# the DB. A relay is the recommended posture; smtp.gmail.com:587 with an app
# password is the simplest. The contact recipient is the `contact_email` site
# setting; while email is unconfigured the contact form falls back to a mailto: link.
# Upgrading from the removed Gmail connect flow: see docs/website/UPGRADE_NOTES.md.
CLIENT_ORIGIN=http://localhost:5173
# Discord bot — internal API (server <-> bot/). BOT_INTERNAL_KEY MUST be
# byte-for-byte identical to the same variable in bot/.env.example — it is the
# only auth on both sides' /internal/* routes, so a mismatch silently breaks
# every server<->bot call with 401s. It also guards the server's
# /internal/bot-config route, which returns the DECRYPTED Discord token: with
# NODE_ENV=production the app REFUSES TO START if this is blank, a documented
# placeholder, or shorter than 16 chars (a warning only in dev). The Discord bot
# TOKEN itself is not an env var — it's entered in the admin panel and stored
# encrypted in the DB (see the bot_config table / SECRET_ENC_KEY above).
BOT_INTERNAL_URL=http://localhost:4100
BOT_INTERNAL_KEY=dev-only-change-me-bot-key
# News announcement pipeline (published news post -> every registered delivery
# leg). The dispatcher is an in-process poller; this tunes it. Links in the
# announcements use APP_BASE_URL (set above), so set that in production too.
#
# Which legs exist depends on what has registered one: Discord (#news) is core's,
# and an installed module may add its own. A module's leg brings its own settings
# with it -- module-uo's in-game town crier reads TOWNCRIER_DURATION_SEC, which is
# documented in that module rather than here, because core has no town crier.
ANNOUNCE_POLL_MS=15000
# Push notifications (M7) — opt-in fan-out to the Android app via a self-hosted
# ntfy UnifiedPush relay (docs/android/PLAN.md §11). The publisher POSTs
# content-free tickles to each device's endpoint, so no publish token is required.
# NTFY_BASE_URL Internal relay URL the publisher POSTs to; also part of the
# backend's SSRF allow-set — a device may only register an
# endpoint on an allowed origin.
# NTFY_PUBLIC_URL Client-facing relay URL surfaced to the app via
# /public/settings.push.ntfyUrl (the app registers its topic
# endpoint here). Defaults to the first NTFY_ALLOWED_ORIGINS
# entry; set when the public URL differs from NTFY_BASE_URL.
# NTFY_ALLOWED_ORIGINS Optional comma-separated allowed origins (the app's endpoint
# must sit on one). Also the default source for NTFY_PUBLIC_URL.
# NTFY_PUBLISH_TOKEN Optional bearer token for backend->ntfy publishes (off by default).
# Leave NTFY_BASE_URL unset in local dev to allow any public HTTPS endpoint
# (private/loopback hosts are always rejected). Without NTFY_PUBLIC_URL /
# NTFY_ALLOWED_ORIGINS the app shows push as unavailable for this instance.
# NTFY_BASE_URL=https://ntfy.example.com
# NTFY_PUBLIC_URL=https://ntfy.example.com
# NTFY_ALLOWED_ORIGINS=https://ntfy.example.com
# NTFY_PUBLISH_TOKEN=
# Modules (MODULE_SYSTEM.md §2.5) — where installable modules live, and where
# they may be installed from.
# MODULES_DIR Directory the loader scans at require time. Defaults to
# <repo>/modules; docker-compose.yml sets it to /app/modules,
# which is the bind mount that makes it meaningful.
# MODULE_SOURCE_HOSTS BOOTSTRAP ONLY. Comma-separated hostnames the admin panel
# may install a module from, seeded into the `module_source_hosts`
# setting the first time the site boots without one. From then
# on the SETTING is authoritative and is edited in
# Admin → Modules — changing this variable on an existing
# deployment does nothing, deliberately, so a redeploy cannot
# silently undo an operator's choice. Installs are https-only
# and an empty list forbids all of them.
# MODULES The module set this deployment RUNS, resolved at every
# start (§2.7.2 decision 4). One entry per module, separated
# by whitespace or commas:
#
# <id>@<version>=<install manifest URL>
#
# A module already unpacked at the declared version is left
# alone WITHOUT touching the network, so a restart with no
# route to the internet comes up unchanged; only a missing or
# different version is fetched, through the same verify-and-
# unpack path (and the same host allowlist) the admin panel
# uses. A version that cannot be fetched is logged and shown
# in Admin → Modules — it never stops the site from starting.
#
# This variable owns what is ON the volume, not what runs: a
# module disabled from the admin panel stays disabled even
# though its files are put back. Leave it unset to manage
# modules entirely from the admin panel.
# MODULES_DIR=/app/modules
# MODULE_SOURCE_HOSTS=gitea.whitlocktech.com
# MODULES=uo@0.3.0=https://gitea.whitlocktech.com/RunicGateway/Module-uo/releases/download/v0.3.0/module-uo-0.3.0.json