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>
177 lines
9.6 KiB
Plaintext
177 lines
9.6 KiB
Plaintext
# ─── Runic Gateway — root environment (used by docker-compose) ───
|
|
# Copy to .env and fill in. NEVER commit the real .env.
|
|
# To run this as an existing branded instance (e.g. UOMysticmoon), see
|
|
# .env.uomysticmoon.example for the exact BRAND_*/DB pinning to copy in.
|
|
|
|
# Container image tag pulled by docker-compose (app + bot). Published by the
|
|
# Gitea Actions workflow on every merge to main as `latest` and `sha-<7>`.
|
|
# Leave as `latest` for routine deploys; pin to a specific build for a
|
|
# reproducible deploy or rollback, e.g. IMAGE_TAG=sha-042a151.
|
|
# Deploy: `docker compose pull && docker compose up -d`.
|
|
IMAGE_TAG=latest
|
|
|
|
# App
|
|
NODE_ENV=production
|
|
PORT=3000
|
|
# Separate, UNPUBLISHED port for server<->bot internal traffic (the decrypted
|
|
# bot-token route). Must match the port in the bot's SITE_INTERNAL_URL
|
|
# (docker-compose.yml) and must NEVER be published/proxied. See issue #33.
|
|
INTERNAL_PORT=3001
|
|
UPLOAD_DIR=/app/uploads
|
|
# Logging — written to BOTH the console and a log file.
|
|
LOG_LEVEL=info # console verbosity: error | warn | info | debug
|
|
FILE_LOG_LEVEL=debug # file verbosity (keep a full record on disk)
|
|
LOG_TO_FILE=true # set false for console-only
|
|
LOG_DIR=/app/logs # log directory inside the container (bind-mounted to ./logs)
|
|
LOG_FILE=app.log
|
|
|
|
# ─── Branding (BRAND_*) ───────────────────────────────────────────────────
|
|
# Instance identity. Defaults render as "Runic Gateway"; set these to rebrand
|
|
# without a rebuild. Text + colors reach the SPA through the settings API at
|
|
# runtime; the server templates index.html <title>/meta/OG/favicon at boot. The
|
|
# admin-editable "site title" and "contact email" settings, if set, override
|
|
# BRAND_NAME / BRAND_CONTACT_EMAIL.
|
|
BRAND_NAME=Runic Gateway
|
|
BRAND_SHORT_NAME=Runic Gateway
|
|
BRAND_TAGLINE=an independent game community
|
|
BRAND_DESCRIPTION=Runic Gateway — an independent game community. News, screenshots, guides, and community notes.
|
|
BRAND_CONTACT_EMAIL=
|
|
BRAND_URL=
|
|
# Accent color — drives the web theme's --accent and the Discord embed color.
|
|
BRAND_ACCENT_COLOR=#7f99bd
|
|
# Image assets: paths under the /brand mount (see docker-compose.yml) or absolute
|
|
# URLs. Blank = built-in defaults (hero falls back to a neutral built-in image).
|
|
BRAND_LOGO=
|
|
BRAND_HERO=
|
|
BRAND_FAVICON=
|
|
|
|
# Database (the values here are shared by the `db`, `app`, and `bot` containers —
|
|
# the bot only ever touches its own tables: guild_config, mod_actions, warnings)
|
|
DB_HOST=db
|
|
DB_PORT=3306
|
|
DB_NAME=runic_gateway
|
|
DB_USER=runic
|
|
DB_PASSWORD=change-me-db-password
|
|
DB_ROOT_PASSWORD=change-me-root-password
|
|
|
|
# Auth
|
|
JWT_SECRET=change-me-to-a-long-random-string
|
|
# Encrypts every secret this site stores at rest (AES-256-GCM): OAuth client
|
|
# secrets, the Discord bot token, the mail transport credentials, the uo-link auth
|
|
# token. REQUIRED in production — with NODE_ENV=production the app REFUSES TO
|
|
# START without it (utils/secretBox.js), so a Compose deployment that leaves it
|
|
# blank crash-loops before it ever listens. Development falls back to a key
|
|
# derived from JWT_SECRET, with a warning.
|
|
#
|
|
# Any string; it is hashed to 32 bytes. Generate a long random one and treat it
|
|
# like the database password.
|
|
#
|
|
# Changing it on a live instance does NOT re-encrypt anything: every secret
|
|
# already stored becomes unreadable and has to be entered again from the admin
|
|
# panel. That is also the reason it is a dedicated key rather than a reuse of
|
|
# JWT_SECRET — rotating a session secret must not orphan stored credentials.
|
|
SECRET_ENC_KEY=change-me-to-a-different-long-random-string
|
|
JWT_EXPIRES_IN=1d
|
|
# auto = Secure cookie only when the request arrives over HTTPS (Pangolin).
|
|
# Leave as auto so login works both via the LAN IP (HTTP) and the proxy (HTTPS).
|
|
COOKIE_SECURE=auto
|
|
# Changing this on a live instance invalidates existing sessions (users re-login).
|
|
COOKIE_NAME=rg_token
|
|
|
|
# Reverse-proxy trust (req.ip / req.secure for rate limiting, backoff, bot-ban).
|
|
# Path: client -> Pangolin -> newt agent "ptero" (separate VM) -> app. Pin this
|
|
# to ptero's LAN IP (e.g. 10.0.0.42) so XFF is only trusted from ptero. Requires
|
|
# a static DHCP reservation for ptero in Omada, else a lease change breaks it.
|
|
# Integer hop count or "false" also accepted; a blanket "true" is rejected
|
|
# (coerced to 1) to prevent X-Forwarded-For spoofing.
|
|
TRUST_PROXY=1
|
|
# Set to 1 to log raw peer address + X-Forwarded-For + resolved req.ip per
|
|
# request (to verify/refresh ptero's IP without redeploying). Noisy; keep off.
|
|
DEBUG_TRUST_PROXY=0
|
|
|
|
# Optional TOTP two-factor (opt-in per user). Defaults to BRAND_NAME when unset.
|
|
# TOTP_ISSUER=Runic Gateway
|
|
TOTP_CHALLENGE_TTL=5m
|
|
|
|
# First admin bootstrap — created only if no users exist yet.
|
|
# Set, run once, then you can blank these out.
|
|
ADMIN_USERNAME=
|
|
ADMIN_PASSWORD=
|
|
|
|
# Email is configured in Admin → Settings → Email, not via env: pick a mail
|
|
# transport (SMTP) and enter its host, port and credentials, which are stored
|
|
# encrypted in the DB. Three postures work — a relay (Mailgun/SES/Postmark) is
|
|
# the recommended one, a mailbox provider over SMTP (e.g. smtp.gmail.com:587
|
|
# with an app password) is the simplest, and an unauthenticated local MTA on
|
|
# port 25 needs no credentials at all. Until one is configured the contact form
|
|
# falls back to a mailto: link (recipient = the `contact_email` site setting).
|
|
# Upgrading from the removed Gmail connect flow: see docs/website/UPGRADE_NOTES.md.
|
|
|
|
# CORS — only needed for local dev when the Vite dev server is a different origin.
|
|
CLIENT_ORIGIN=http://localhost:5173
|
|
|
|
# Discord bot — internal API (server <-> bot/, see docker-compose.yml's `bot`
|
|
# service). 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 left blank, at this placeholder, or shorter than 16 chars. Generate a
|
|
# long random string. The Discord bot TOKEN itself is not an env var — it's
|
|
# entered in the admin panel (Discord Bot page) and stored encrypted in the DB.
|
|
#
|
|
# Defense in depth: even with a strong key, configure Pangolin/your reverse
|
|
# proxy to DENY /api/v1/internal (and never forward INTERNAL_PORT). The route no
|
|
# longer rides the public listener, but an explicit deny rule is belt-and-braces.
|
|
BOT_INTERNAL_URL=http://bot:4100
|
|
BOT_INTERNAL_KEY=change-me-to-a-long-random-string
|
|
|
|
# ─── Installed modules ───
|
|
# A module is a directory on the modules volume (see MODULES_DIR in
|
|
# server/.env.example); everything about a specific game lives in one, and core
|
|
# knows nothing about any of them. A module may read its own env vars, and they
|
|
# belong here because Compose passes this file to the container.
|
|
#
|
|
# MODULES declares the set this deployment runs, and the container arrives at it
|
|
# on its own — no admin panel, no `tar -xf` on the host. One entry per module,
|
|
# `<id>@<version>=<install manifest URL>`, whitespace- or comma-separated:
|
|
#
|
|
# MODULES=uo@0.3.0=https://gitea.whitlocktech.com/RunicGateway/Module-uo/releases/download/v0.3.0/module-uo-0.3.0.json
|
|
#
|
|
# A module already unpacked at the declared version is a no-op that never touches
|
|
# the network, so a restart with the internet down brings the site up exactly as
|
|
# it was; only a missing or different version is fetched, verified against the
|
|
# sha256 its manifest declares, and unpacked. A failure is logged and shown in
|
|
# Admin → Modules, and the site starts anyway. The variable owns what is on the
|
|
# volume, not what runs — a module disabled from the admin panel stays disabled.
|
|
# Leave it unset to install from the admin panel instead.
|
|
#
|
|
# RunicGateway/Module-uo, for example, reads UOLINK_BASE_URL / UOLINK_WS_URL /
|
|
# UOLINK_PROTOCOL as the defaults for its connection to a uo-link sidecar, and
|
|
# TOWNCRIER_DURATION_SEC for its news leg. Its README documents them; they are
|
|
# left out here rather than half-copied, because a copy of another repo's
|
|
# settings is a copy that goes stale silently. With no module installed, none of
|
|
# this applies and the site runs as core.
|
|
|
|
# ─── Push notifications (M7) — self-hosted ntfy UnifiedPush relay ───
|
|
# The `ntfy` compose service and the backend's push fan-out (opt-in notifications
|
|
# for the Android app; docs/android/PLAN.md §11).
|
|
# NTFY_BASE_URL Public URL devices reach the relay at (behind the
|
|
# reverse proxy). Used BOTH to configure the ntfy service
|
|
# AND as the backend's SSRF allow-set — a device may only
|
|
# register an endpoint whose origin matches this.
|
|
# NTFY_ALLOWED_ORIGINS Optional, comma-separated extra allowed endpoint origins
|
|
# (defaults to NTFY_BASE_URL's origin). Set only if devices
|
|
# register endpoints on a different host than NTFY_BASE_URL.
|
|
# NTFY_PUBLISH_TOKEN Optional. The content-free-tickle design needs NO token;
|
|
# set one only to require auth on backend→ntfy publishes.
|
|
# NTFY_HOST_PORT Host port the ntfy container publishes :80 on (default
|
|
# 2586). The public reverse proxy forwards the notification
|
|
# subdomain to host:NTFY_HOST_PORT — required because the
|
|
# proxy lives outside the compose network and cannot reach
|
|
# ntfy any other way. Change only on a host-port conflict.
|
|
NTFY_BASE_URL=https://ntfy.example.com
|
|
# NTFY_ALLOWED_ORIGINS=https://ntfy.example.com
|
|
# NTFY_PUBLISH_TOKEN=
|
|
# NTFY_HOST_PORT=2586
|