# World of Claudecraft server configuration — copy to .env and edit: # cp .env.example .env # The game server loads .env automatically; docker compose reads it too. # Postgres password for the eastbrook database (docker compose requires it). # Make it long and random for anything reachable from the internet. POSTGRES_PASSWORD=change-me # Connection string the game server uses. Only needed when running # `npm run server` outside docker — compose builds its own from # POSTGRES_PASSWORD. The dev database from `npm run db:up` listens on # 127.0.0.1:5433; keep this password in sync with POSTGRES_PASSWORD. DATABASE_URL=postgres://eastbrook:change-me@127.0.0.1:5433/eastbrook # HTTP + WebSocket port for the game server (default 8787). #PORT=8787 # Community test realms only: provision every newly created account with one # level-20 character per class, complete Warfare equipment, and four maximum # bags. Off by default. Do not enable this on a normal production realm. #PROVISION_TEST_ACCOUNTS=0 # Host directory mounted at /app/dist/media so content-hashed media files stay # available across container replacements. The Ansible deploy sets this to an # absolute /opt/eastbrook path in production. EASTBROOK_MEDIA_DIR=./media-cache # Host static-overlay root for Studio production SFX packs. The compose server # reads its audio/sfx child and keeps it outside replaceable container images. EASTBROOK_SFX_DIR=./sfx-runtime # MediaWiki runs as a separate service for the player wiki. # Local URL: http://localhost:8080/wiki/ MEDIAWIKI_DB_NAME=mediawiki MEDIAWIKI_DB_USER=mediawiki MEDIAWIKI_DB_PASSWORD=mediawiki-change-me MEDIAWIKI_ADMIN_USER=WikiAdmin MEDIAWIKI_ADMIN_PASS=change-me-admin-password # IMPORTANT (production): set this to the PUBLIC origin, NOT localhost. MediaWiki # builds absolute URLs / redirects from it, so a localhost value sends real # visitors to http://localhost:8080/... The reverse proxy must route /wiki* to # this service (see DEPLOY.md). e.g. MEDIAWIKI_SERVER=https://worldofclaudecraft.com MEDIAWIKI_SERVER=http://localhost:8080 # Optional production secrets. Set long random values before exposing publicly. #MEDIAWIKI_SECRET_KEY= #MEDIAWIKI_UPGRADE_KEY= # Game-server redirect target when someone opens /wiki on the game port directly. # In production set this to the public wiki URL, NOT localhost — otherwise the # fallback below 302-redirects visitors to their own machine. # e.g. WIKI_URL=https://worldofclaudecraft.com/wiki/index.php/Main_Page WIKI_URL=http://localhost:8080/wiki/index.php/Main_Page # Realm (world/shard) this process serves (default "Claudemoon"). Characters, # friends, guilds, and presence are all scoped to it. To run multiple isolated # realms, start one process per realm — same DATABASE_URL, a different # REALM_NAME and PORT each — e.g. REALM_NAME=Ironforge PORT=8788. Letters, # digits, spaces, and ' _ - up to 24 chars; invalid values fall back to default. #REALM_NAME=Claudemoon # Postgres clients THIS realm process keeps in its pool (default 10, accepted # range 1 to 97, strict decimal parsing). Nothing is clamped: an out-of-range or # malformed value FALLS BACK to the default and logs an error at boot, and the # effective pool size is logged at boot either way. Every realm process builds # its own pool on the one DATABASE_URL with no cross-process coordination, and # stock postgres:16 serves max_connections 100 with 3 superuser-reserved: keep # realms x DB_POOL_MAX_CLIENTS + tooling at or under 97, plus one more per realm # for the dedicated boot client that applies the schema, or logins fail with # "too many clients" exactly at peak. Connections are not the binding # constraint, though: the game process and Postgres share one 4-vCPU box where # the database is already the heaviest CPU consumer at peak, so raise this only # against a measured pool-exhaustion symptom, a few clients at a time. #DB_POOL_MAX_CLIENTS=10 # Type of this process's own realm (Normal | PvP | RP | RP-PvP). Default Normal. # Only used for the single-realm default directory; multi-realm uses REALMS. #REALM_TYPE=Normal # Optional realm directory powering the client's WoW-style Realm List screen. # Comma-separated `Name=https://origin=Type` entries (Type optional → Normal); # every realm process should advertise the same list. Unset → a single # same-origin realm. Cross-origin realms here are also the CORS allowlist. #REALMS=Claudemoon=https://claudemoon.example.com=Normal,Ironforge=https://ironforge.example.com=PvP # Public origin used for absolute server-generated URLs such as player-card # Open Graph metadata. In multi-realm deployments, the matching REALMS entry is # used when this is unset. Set this for single-realm production. #PUBLIC_ORIGIN=https://worldofclaudecraft.com # How many days of chat logs to keep (default 90; 0 = keep forever). #CHAT_LOG_RETENTION_DAYS=90 # How many days of client performance telemetry to keep (default 14; 0 = keep # forever). The UA cohort reports correlate device tier and FPS with early # retention, which needs at least a month of history: production runs 90. #PERF_REPORT_RETENTION_DAYS=90 # How many days of unstuck-recovery telemetry to keep (default 90; 0 = keep # forever). The admin unstuck-reports view and hotspot readout cap their read # window at 90 days regardless (UNSTUCK_REPORT_MAX_DAYS), so retention above # 90 keeps rows the admin views cannot display. Rows are account-linkable # (account and character ids plus positions): audit a keep-forever setting # with the same care as the IP tables (DEPLOY.md, retention knobs). #UNSTUCK_REPORT_RETENTION_DAYS=90 # How many days of the daily-reward point-event audit ledger to keep (default 400; # 0 = keep forever). Bounds how far back the admin per-account point-event history # can look. #DAILY_REWARD_EVENTS_RETENTION_DAYS=400 # How many days of admin online-population samples to keep (default 90; 0 = keep # forever). The all-time peak is folded forward before any pruning. #ONLINE_SAMPLES_RETENTION_DAYS=90 # How many days of site-presence samples and visitor sessions to keep (default 90; # 0 = keep forever). #SITE_PRESENCE_RETENTION_DAYS=90 # How many days of raw play sessions to keep before they fold into the lifetime # rollups (default 180; 0 = keep forever; positive values below 45 are raised to 45). #PLAY_SESSION_RETENTION_DAYS=180 # How many days a folded account-to-IP association may go unseen before it is # deleted (default 730; 0 = keep forever). This key alone bounds how long the # account-to-IP link (personal data) is retained, so 0 here forfeits that # privacy cap: trim the other retention keys to reclaim disk instead. #ACCOUNT_IP_ASSOCIATION_RETENTION_DAYS=730 # How many days of per-account daily activity rows (the business-metrics fact # table player_activity_daily) to keep (default 400; 0 = keep forever). The # business snapshots read only the last two days, so any positive window is # read-invisible to them. #PLAYER_ACTIVITY_RETENTION_DAYS=400 # How many days of password-reset / email-change request rows to keep after # creation (default 30 each; 0 = keep forever). Requesting a new reset or email # change supersedes (deletes) any still-pending row for the same account, but # that only bounds duplicate PENDING rows: a consumed or abandoned-and-expired # one is untouched by it, so these two keys are what actually age the tables. #PASSWORD_RESET_REQUEST_RETENTION_DAYS=30 #EMAIL_CHANGE_REQUEST_RETENTION_DAYS=30 # How many days of the outbound-email audit trail (email_log: every send # attempt, success or failure) to keep (default 90; 0 = keep forever). #EMAIL_LOG_RETENTION_DAYS=90 # How many days of RESOLVED player_reports rows (status ignored or actioned) # to keep (default 180; 0 = keep forever). A still-open report is never # pruned regardless of age: the moderation queue and the per-account report # view only ever read open rows. #PLAYER_REPORT_RETENTION_DAYS=180 # How many days of bug_reports rows to keep (default 90; 0 = keep forever). # Each row can carry a screenshot up to ~900 KB, so this is the # fastest-growing of the report tables. #BUG_REPORT_RETENTION_DAYS=90 # How many days of the chat_violations hard-word incident log to keep # (default 90; 0 = keep forever). The only reader is already limited to a # bounded per-account page, so pruning the oldest rows never invalidates it. #CHAT_VIOLATION_RETENTION_DAYS=90 # The request header carrying the visitor's ISO 3166-1 alpha-2 country code, # injected by a TRUSTED edge (Cloudflare: cf-ipcountry, the default; nginx # with the geoip2 module: whatever header the site config sets). Stored on # accounts.created_country at signup for UA geo cohorts; analytics only, # never authorization. Set empty to disable the read entirely. Do not point # this at a client-forgeable header on an edge that does not strip it. #GEOIP_COUNTRY_HEADER=cf-ipcountry # How many days of the progression analytics event logs to keep (defaults 365 # and 90; 0 = keep forever). level_up_events is one row per player level-up # and feeds multi-month friction maps. ftue_events records new-player quest # and death events (level-gated, first-touch-unique per quest, deaths capped # per character); it answers a first-session question and has the higher # intake, so it keeps a quarter by default. #LEVEL_UP_EVENTS_RETENTION_DAYS=365 #FTUE_EVENTS_RETENTION_DAYS=90 # UTC hour (0-23) the nightly retention sweep runs (default 5). Unlike the # retention keys, an explicit 0 here is a live value: it moves the sweep to # 00:00 UTC. #RETENTION_SWEEP_UTC_HOUR=5 # Per-table row budget for each nightly sweep run (default 50000). An explicit 0 # disables pruning for the run; leave unset for the default. #RETENTION_SWEEP_MAX_ROWS_PER_RUN=50000 # Comma-separated reverse-proxy IPs whose X-Forwarded-For header is trusted # when rate-limiting. Default trusts loopback + private ranges, which is # right for the compose + local reverse proxy setup; pin an explicit list # if your proxy connects from elsewhere. #TRUSTED_PROXY_IPS= # Cloudflare Turnstile secret key. Gates POST /api/register and POST /api/login: # when set, requests must carry a Turnstile token that verifies against # Cloudflare, which blocks headless bot clients (they cannot solve the challenge). # UNSET = verification skipped entirely (the local/dev default), so the gate ships # DISABLED unless you set this in production. Pair it with VITE_TURNSTILE_SITEKEY. #TURNSTILE_SECRET= # Secret for POST /internal/restart-countdown. The deploy playbook calls this # endpoint before restarting production so players see warnings. The secret # header is the ONLY gate (the edge does not hide /internal/*), so treat it as # a real production secret: set a long random value, mirror it in Ansible # secrets. #RESTART_COUNTDOWN_SECRET= # Opt-in world-loop perf diagnostics. PERF_TICK_LOG=1 makes the game server log # a steady 20 Hz loop heartbeat (tickHz, per-phase p95/max) plus a throttled # trace whenever a tick blows its 50 ms budget. Leave unset in normal operation # so production logs stay quiet. #PERF_TICK_LOG= # Node flags for the game server process, chiefly the V8 heap ceiling. Node derives # its default old-space size from TOTAL SYSTEM memory, not from the container memory # limit, so on a box bigger than that limit an unset heap flag lets V8 grow past the # limit and get OOM-killed mid-tick by the kernel. Keep the heap under the container's # mem_limit (5g in docker-compose.yml) so an overrun kills the container, which docker # restarts, rather than wedging the host. Uncomment the line below in production; # unset (the default) means node's own defaults, which is right for local dev. #NODE_OPTIONS=--max-old-space-size=4096 # Bearer token that gates GET /metrics (the Prometheus RED + process exposition). # Unset (the default) leaves /metrics feature-off (404); a non-empty value turns # it on behind Authorization: Bearer . Set to a long random value and # mirror it in the monitoring scrape secret. Leave unset for local dev. #METRICS_TOKEN= # The realm player admission cap. A fresh WebSocket join is refused once the realm # holds this many sessions (server/ws_auth.ts); GET /api/status advertises the value # so the client realm list can display Full honestly. Unset (empty) means the # built-in default of 5000 (deliberately far above current hardware capacity, so the # default never turns a player away; set a lower value here to enforce a measured # ceiling); set an explicit 0 to disable the cap entirely. #MAX_PLAYERS_PER_REALM= # Discord integration. OAuth in the game server is enabled when # DISCORD_CLIENT_ID and DISCORD_CLIENT_SECRET are set. The separate bot service # is opt-in so local/self-hosted compose stacks keep working without Discord # credentials; enable it with COMPOSE_PROFILES=discord after setting the bot env. # Register this callback in the Discord application: # https://your-domain.example/api/auth/discord/callback # Auto-join: set DISCORD_BOT_TOKEN in the GAME SERVER env too (not just the bot) to # add players to DISCORD_GUILD_ID for them on Discord link/login. The server then # requests the guilds.join scope, and the OAuth callback calls PUT # /guilds/{id}/members/{id}. The bot must be a guild member with the Create Invite # permission. Leave it unset here and players just use the invite link to join. #COMPOSE_PROFILES=discord #PUBLIC_GAME_URL=https://worldofclaudecraft.com #DISCORD_CLIENT_ID= #DISCORD_CLIENT_SECRET= #DISCORD_GUILD_ID= #DISCORD_GUILD_INVITE=https://discord.com/invite/worldofclaudecraft #DISCORD_BOT_TOKEN= #DISCORD_BOT_SECRET= #DISCORD_VOICE_CHANNEL_ID= #DISCORD_WELCOME_CHANNEL_ID= #DISCORD_TEST_CHANNEL_ID= #DISCORD_RELAY_CHANNEL_ID= #DISCORD_ACTIVITY_CHANNEL_ID= # Achievements channel used for daily rewards top-10 winner posts. #DISCORD_DAILY_REWARDS_CHANNEL_ID= #DISCORD_SYNC_NICKNAMES=1 # Rate-limit governor (bot service). Each falls back to the default shown when # unset, empty, or non-numeric, never to 0. Send rate is per second (Discord's # own ceiling is 50); the ban pause is the process-wide stop after a non-JSON 429; # the breaker limit is invalid requests per 10 minutes before sweeps stop; the # forbidden TTL is how long a member's 401 or 403 is remembered. #DISCORD_MAX_RPS=8 #DISCORD_BAN_PAUSE_MS=600000 #DISCORD_BREAKER_LIMIT=300 #DISCORD_FORBIDDEN_TTL_MS=86400000 # Developer badge (GitHub). The in-game developer badge credits a player for the # commits they have landed in the open-source repo. Linking is enabled when # GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET are set (register a GitHub # OAuth App and add this callback URL to it): # https://your-domain.example/api/auth/github/callback # Without these the "Link GitHub" UI is hidden, exactly like Discord. GITHUB_REPO # selects which repo's contributor stats back the badge (default # levy-street/world-of-claudecraft); the same optional GITHUB_TOKEN used by the # News/Releases proxy raises the GitHub API rate limit for the contributor fetch. #GITHUB_OAUTH_CLIENT_ID= #GITHUB_OAUTH_CLIENT_SECRET= #GITHUB_REPO=levy-street/world-of-claudecraft #GITHUB_TOKEN= # Cloudflare Turnstile SITE key (public) for the login/register widget. NOTE: this # is read by the CLIENT and inlined at BUILD time (`vite build`/`npm run build`), # NOT at server runtime — it must be present in the build environment, not just on # the server. Unset → the widget never renders and no token is sent. Use a separate # Turnstile widget (key pair) per environment; dev/prod differ. See DEPLOY.md. #VITE_TURNSTILE_SITEKEY= # Native mobile app attestation for login/register. In production # (NODE_ENV=production), native app requests from Capacitor origins must carry a # valid platform attestation proof instead of a Turnstile token. Set # NATIVE_ATTESTATION_REQUIRED=0 only as an emergency rollback. #NATIVE_ATTESTATION_REQUIRED=1 # Google Play Integrity verification for Android native builds. The package name # should match android/app/build.gradle applicationId. The certificate digest is # the Play app-signing certificate SHA-256 digest from Play Console. #GOOGLE_PLAY_INTEGRITY_SERVICE_ACCOUNT_JSON={"client_email":"...","private_key":"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"} #GOOGLE_PLAY_INTEGRITY_SIGNING_PEM=-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n #GOOGLE_PLAY_INTEGRITY_PACKAGE_NAME=com.worldofclaudecraft #GOOGLE_PLAY_INTEGRITY_CERT_DIGESTS= #GOOGLE_PLAY_INTEGRITY_DEVICE_VERDICT=MEETS_DEVICE_INTEGRITY # Solana Store native authentication, Seeker claim, and Daily Rewards spin fail # closed unless these artifact values are configured. The certificate list must # contain only SHA-256 digests for the Solana Store release signing artifact, # never Play signing certificates. A correctly signed sideload uses the same # artifact. #SEEKER_SOLANA_INTEGRITY_PACKAGE_NAME=com.worldofclaudecraft #SEEKER_SOLANA_INTEGRITY_CERT_DIGESTS= #SEEKER_SOLANA_INTEGRITY_DEVICE_VERDICT=MEETS_DEVICE_INTEGRITY # Optional Android build-time override for direct-installed/native test builds. # Defaults to the World of ClaudeCraft Google Cloud project number. #PLAY_INTEGRITY_CLOUD_PROJECT_NUMBER=865860061593 # Apple DeviceCheck verification for iOS native builds. APPLE_TEAM_ID is your # Apple Developer Team ID. APPLE_DEVICECHECK_KEY_ID is the Key ID for a DeviceCheck # key created under Certificates, Identifiers & Profiles > Keys. Store the .p8 # contents as escaped newlines, not as a local file path. #APPLE_TEAM_ID=9RT4S4TGA3 #APPLE_BUNDLE_ID=com.worldofclaudecraft #APPLE_DEVICECHECK_KEY_ID= #APPLE_DEVICECHECK_SIGNING_PEM=-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n #APPLE_DEVICECHECK_ENV=production # Native Sign in with Apple token audience. Optional for the production bundle, # which defaults to com.worldofclaudecraft. Set this when using another bundle ID. #APPLE_CLIENT_ID=com.worldofclaudecraft # DANGER: enables the level/teleport/item cheat commands used by the test # bots (scripts/crypt_raid.mjs etc). Never set this on a public server. #ALLOW_DEV_COMMANDS=1 # Steam account linking + achievement mirror (server/steam/). OFF by default: # with STEAM_ENABLED unset, every /api/steam route answers steam.disabled, the # mirror is inert, and no client renders link UI. When enabling, provide the # Steamworks app id and a PUBLISHER Web API key (partner.steam-api.com); the # key must never appear in logs or client code. Linking is a cosmetic mirror # for deed achievements only; login with Steam does not exist. #STEAM_ENABLED=1 #STEAM_APP_ID= #STEAM_WEB_API_KEY= # ElevenLabs API key — used ONLY by the offline NPC-voice generation scripts # (scripts/gen_npc_voices.mjs designs the voices, scripts/gen_npc_lines.mjs # synthesizes the line mp3s into public/audio/voice). Not read by the game server # or client at runtime. Generate one at https://elevenlabs.io and keep it secret. #ELEVENLABS_API_KEY= # Public Reown project id used by desktop website WalletConnect QR codes. Create one at # https://cloud.reown.com and allow each website origin # that builds the client (production, staging, and local development). This id # is intentionally bundled into the client and is not a secret. Injected Wallet # Standard extensions and the direct Phantom/Solflare mobile-web handoffs still # work when it is unset, but website external app/QR connect is not offered. The # macOS/Windows desktop client uses the normal browser and installed extensions, # so its browser handoff does not require another environment variable. #VITE_REOWN_PROJECT_ID= # Optional client-side kill switch for every non-custodial Solana wallet UI. # Set this only if a deploy should hide both injected and external wallet flows. #VITE_WALLET_DISABLED=1 # Solana RPC endpoint for reading $WOC balances (mainnet). SERVER-SIDE ONLY: the # client reads balances through the server proxy (GET /api/woc/balance), so this # URL (and any API key in it, e.g. Helius/QuickNode/Triton) is NEVER shipped in # the client bundle. The public default is heavily rate-limited; set a dedicated # endpoint in production. NOT prefixed VITE_: keep it off the client. #SOLANA_RPC_URL=https://api.mainnet-beta.solana.com # The $WOC SPL token mint the server reads balances against. Defaults to the # published contract; override only if the canonical mint changes. Server-side. #WOC_MINT=3WjLscH2JsXLEFJZRA9z8ti8yRGxWGKbqymPd7UicRth # (For local dev, server/db.ts also loads .env.local, so VITE_SOLANA_RPC_URL / # VITE_WOC_MINT there are still honored as a fallback, but no client code reads # them, so they are never inlined into the build.) # Daily rewards. Runtime reward settings, WOC/SOL prices, and tasks are owned by # the private payout service. The public API only needs the service origin and # shared secret. #WOC_DAILY_REWARD_SERVICE_URL=http://127.0.0.1:8797 #WOC_DAILY_REWARD_SERVICE_SECRET=local-dev-secret #WOC_DAILY_REWARD_CONFIG_TTL_MS=300000 # Rift forge wire commands (upgrade / enchant / socket on Riftbound bands). # The client forge UI has not shipped, so the server refuses the three wire # commands unless this is exactly 1 (server/rift_forge_gate.ts). Leave unset # in production until the forge intentionally ships; enable only on PTR or # internal playtest realms. #RIFT_FORGE_ENABLED=1 # Optional Rift Dungeon Upgrader. Server-side only: never prefix these with # VITE_. A dedicated service takes precedence when RIFT_UPGRADER_URL is set. #RIFT_UPGRADER_URL= #RIFT_UPGRADER_API_KEY= #RIFT_UPGRADER_TIMEOUT_MS=20000 #RIFT_UPGRADER_MAX_REQUESTS_PER_HOUR=4 # Direct OpenAI Responses mode is used only when the dedicated URL is unset and # both OPENAI_API_KEY (from server secret storage) and RIFT_UPGRADER_MODEL exist. #RIFT_UPGRADER_MODEL= #RIFT_UPGRADER_OPENAI_URL=https://api.openai.com/v1/responses # Optional runtime asset-job bridge. Generated binaries are not hot-loaded; # successful jobs still require QA promotion into the immutable asset manifest. #RIFT_RUNTIME_ASSETS=0 #RIFT_ASSET_PIPELINE_URL= #RIFT_ASSET_PIPELINE_API_KEY= #RIFT_ASSET_TIMEOUT_MS=60000 #RIFT_ASSET_MAX_REQUESTS_PER_EVENT=1 # Claudium economy service. Server-side only: the browser talks to # /api/claudium/* on the game server, and the game server proxies to this # internal service with the shared secret. The loopback example is for a game # server started directly on the host. With Docker Compose, use # http://host.docker.internal:8798/v1/claudium/ for a host-run service, or use # the economy service's remote/internal DNS name. #WOC_ECONOMY_SERVICE_URL=http://127.0.0.1:8798/v1/claudium/ #WOC_ECONOMY_INTERNAL_SECRET=local-dev-secret # Meta Conversions API. Server-side only: never prefix these with VITE_. # META_CAPI_ACCESS_TOKEN enables CAPI for AccountCreated and ReachedLevel5; # unset keeps local/dev builds on browser Pixel only. Use TEST_EVENT_CODE only # while validating in Meta Events Manager. #META_CAPI_PIXEL_ID=1692101265042180 #META_CAPI_ACCESS_TOKEN= #META_CAPI_TEST_EVENT_CODE= #META_CAPI_API_VERSION=v21.0 # Outbound email (account lifecycle, password reset, security notices; see # server/email/). With nothing set the server uses the console transport: every # email is logged, none are sent: the right default for local dev and tests. # # AWS SES (recommended for production): set EMAIL_PROVIDER=ses, a region, and # the From address. Credentials come from the AWS SDK default chain: on EC2 # prefer an IAM instance role with ses:SendEmail (no keys in this file); the # AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY pair is the fallback elsewhere. The # sending domain must be a verified SES identity with production access. #EMAIL_PROVIDER=ses #EMAIL_SES_REGION=us-east-1 #EMAIL_FROM=World of ClaudeCraft # Base URL for links inside emails (verify, unsubscribe). Falls back to # PUBLIC_BASE_URL, then https://worldofclaudecraft.com. #EMAIL_BASE_URL=https://worldofclaudecraft.com # Generic HTTP transport (any provider with a JSON POST + bearer-key API, e.g. # Resend). Used when EMAIL_PROVIDER is unset and all three values are present. #EMAIL_API_URL=https://api.resend.com/emails #EMAIL_API_KEY= # Self-hosted OTA updates for the native mobile shells (docs/ota-updates.md). # The game server's POST /api/ota/updates endpoint reads the published bundle # manifest from this URL (must be https; cached 60 s). Unset = OTA off: the endpoint answers # "no update" and the native apps behave as if updates were not configured. #OTA_MANIFEST_URL=https://updates.worldofclaudecraft.com/ota/latest.json # Ops kill switch for the DIFFERENTIAL update channel only: set to 1 and every # offer degrades to the full-zip form (no embedded per-file manifest) without # touching the update channel itself. Must be listed in the docker-compose # game-service environment allowlist to reach the process, like OTA_MANIFEST_URL. #OTA_DELTA_DISABLED=1 # The OTA publish script (npm run ota:publish) reads these; they are deploy-time # tooling config, not game-server config. Credentials come from the AWS CLI # default chain (a profile with write access to the bucket). # # The store is the Cloudflare R2 bucket that already serves desktop updates on # https://updates.worldofclaudecraft.com, under a separate ota/ prefix, so no new # bucket or CDN is needed. OTA_S3_ENDPOINT_URL is what points the AWS CLI at R2 # instead of AWS S3; leave it unset to publish to real S3. #OTA_S3_BUCKET=worldofclaudecraft-updates #OTA_S3_PREFIX=ota #OTA_PUBLIC_BASE_URL=https://updates.worldofclaudecraft.com # (the R2 endpoint is https://.r2.cloudflarestorage.com; # the real account id lives in the private deploy inventory, never in this repo) #OTA_S3_ENDPOINT_URL=https://ACCOUNT_ID.r2.cloudflarestorage.com # Optional: the oldest native shell version the published bundle still works # on; older shells are steered to the app stores by the existing update prompt. #OTA_MIN_NATIVE_VERSION=0.32.0