# ─── 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 /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). # -> 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 (Gmail over OAuth2), not here. # It reuses the Google auth provider's OAuth client and stores an encrypted # refresh token in the DB. The contact recipient is the `contact_email` site # setting; while email is unconfigured the contact form falls back to a mailto: link. 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 -> in-game town crier + Discord # #news). The dispatcher is an in-process poller; these tune it. Links in the # announcements use APP_BASE_URL (set above), so set that in production too. # ANNOUNCE_POLL_MS how often the dispatcher sweeps for due/retry legs # TOWNCRIER_DURATION_SEC how long the in-game town-crier message stays up (<= 86400) ANNOUNCE_POLL_MS=15000 TOWNCRIER_DURATION_SEC=3600 # 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 the shard. # NTFY_BASE_URL=https://ntfy.example.com # NTFY_PUBLIC_URL=https://ntfy.example.com # NTFY_ALLOWED_ORIGINS=https://ntfy.example.com # NTFY_PUBLISH_TOKEN=