Skip to content

Local Development Setup ​

This document explains how to configure your local environment to run FantasyWiki with a real Google login and MSW-mocked API data. These files are intentionally gitignored and must be created manually by each developer.


Overview ​

The project uses two environment layers:

FileLocationPurpose
backend/.dev.varsbackend/Secrets for Wrangler (backend)
frontend/.env.localfrontend/Overrides for Vite (frontend)

Neither file is committed to the repository; a .example sits beside each one as its checked-in shape, so the quickest start is:

bash
cp backend/.dev.vars.example backend/.dev.vars
cp frontend/.env.local.example frontend/.env.local

Then fill in the two blanks in .dev.vars (see Step 1). Both files are loaded automatically by their respective tools at startup.

You do not need a Cloudflare account to run FantasyWiki locally. The one feature that needs one, the Article Genie, is switched off by default and the app hides it; see Running without a Cloudflare account.

To skip installing Node and npm as well, and to skip GOOGLE_CLIENT_SECRET entirely, see Running FantasyWiki in Docker instead.


Step 1, Create backend/.dev.vars ​

Create the file at FantasyWiki/backend/.dev.vars with the following content:

ini
GOOGLE_CLIENT_ID=457796621894-7krpa8n09mekpj2cdd952jjkf1c3ookt.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=<ask a team member>
JWT_SECRET=<any random string, at least 32 characters>
FRONTEND_URL=localhost:5173
GH_APP_PRIVATE_KEY=<optional; only needed to exercise the problem report form>

Notes:

  • GOOGLE_CLIENT_ID is the project's public Google OAuth client ID. In CI it is injected per-environment as a secret (GOOGLE_CLIENT_ID_PREVIEW / GOOGLE_CLIENT_ID_PRODUCTION); locally it must be set here in .dev.vars (it is no longer in wrangler.jsonc).

  • FRONTEND_URL is passed as a --var at deploy time; locally it must be set here too.

  • JWT_SECRET can be any random string. Generate one with:

    bash
    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
  • GOOGLE_CLIENT_SECRET is sensitive: ask a team member or find it in the Google Cloud Console under the OAuth 2.0 client for this project. It is the only value here you cannot produce yourself, and you can leave it blank: set VITE_DEV_LOGIN=true in Step 2 and sign in as the demo player instead.

  • GH_APP_PRIVATE_KEY backs the in-app problem report form (/report), which opens an issue on FantasyWiki/FantasyWiki as FantasyWiki[bot]. Leave it unset unless you are working on that form, every submission with a valid key files a real issue (labelled preview outside production, so it stays filterable and bulk-deletable).

    It is the GitHub App's private key, converted to PKCS#8 (GitHub gives you PKCS#1, which WebCrypto cannot import), with newlines escaped as \n so it fits on one line. See Problem Reports for how to create the App and convert the key. GH_APP_ID, GH_APP_INSTALLATION_ID, GITHUB_REPO and ENVIRONMENT are plain vars and already live in wrangler.jsonc.

    In CI it is a GitHub Actions secret of the same name, pushed to the Worker by the deploy workflow, not a wrangler secret put.

  • Nothing in this file is a Cloudflare credential. Workers AI is bound to the Worker rather than keyed, which is why it is switched off by environment rather than by secret.


Step 2, Create frontend/.env.local ​

Create the file at FantasyWiki/frontend/.env.local with the following content:

ini
VITE_BACKEND_URL=http://127.0.0.1:8787
VITE_MOCK=true
# VITE_DEV_LOGIN=true
# VITE_PASSWORD_AUTH=true

Notes:

  • VITE_BACKEND_URL is used as-is by the frontend runtime, so include http:// explicitly for local backend.
  • VITE_MOCK=true enables MSW (Mock Service Worker), which intercepts all API calls except /api/v1/session and /auth/*, which pass through to the real local backend.
  • VITE_DEV_LOGIN=true puts a Continue as demo player button on the login screen, which mints the same session Google would without going near Google, so you can leave GOOGLE_CLIENT_SECRET blank and skip Step 3 entirely. The button only exists in a build started with the variable set, and the backend answers /auth/dev with a 404 unless it is running as --env local, so neither side relies on the other. It is what the containers use; see Running FantasyWiki in Docker.
  • VITE_PASSWORD_AUTH=true puts a username and password form on the login screen. It works only against a backend started with npm run devmongo (or ./gradlew devMongo): username/password sign-in lives in src/indexPassword.ts, which only wrangler.mongo.jsonc names, so the D1 local run, and every deployment, does not contain those routes at all. Never set it in .env.preview or .env.production. See Auth Modes.

Step 3, Add Yourself to Google OAuth Redirect URIs ​

Google blocks logins from unregistered redirect URIs. Each developer must add their local redirect URI to the Google Cloud Console once.

  1. Go to Google Cloud Console → Credentials
  2. Open the OAuth 2.0 Client ID for FantasyWiki
  3. Under Authorized redirect URIs, add:
    http://127.0.0.1:8787/auth/google
  4. Save and wait ~1 minute for propagation.

Step 4, Start the App ​

Open two terminals:

bash
# Terminal 1, backend
cd backend
npm run dev          # wrangler dev --env local
# Should print: Ready on http://127.0.0.1:8787
bash
# Terminal 2, frontend
cd frontend
npm run dev
# Should print: Local: http://localhost:5173/

Or from the project root using Gradle:

bash
./gradlew devMock

Wait for both "Ready" messages before opening the browser.

Running on MongoDB instead of D1 ​

The backend persists to either Cloudflare D1 or MongoDB (Persistence Targets), and the local run can be either. MongoDB needs a replica set to talk to, the guarded writes are transactions, and a standalone mongod has none. One node is enough:

bash
docker run -d --name fw-mongo -p 27017:27017 mongo:8 --replSet rs0 --bind_ip_all
docker exec fw-mongo mongosh --quiet --eval 'rs.initiate()'

Then start the backend against it, from backend/:

bash
npm run devmongo    # wrangler dev --config wrangler.mongo.jsonc

Or, with the frontend beside it, from the project root:

bash
./gradlew devMongo

There is nothing to migrate first: npm run dev applies the D1 migrations before starting, and the MongoDB run writes its indexes and its baseline, the Global League, the system player, the en scale factor, on its first connection. The database is fantasywiki on 127.0.0.1:27017; change MONGO_URL in backend/wrangler.mongo.jsonc to point somewhere else.

Both runs share .dev.vars, so the same secrets and the same Google sign-in work either way, and neither can see the other's data.

The MongoDB config carries only what a local session uses, which is less than env.local binds:

  • No D1, and no settlement Workflow or cron: that is started by a daily trigger, which does not fire locally.
  • AI, bound, so the Article Genie is the real thing here, against the model ADR 0006 measured. Workers AI has no local simulator, so a Genie call leaves the machine and spends the account's neuron allocation even in dev, which is why the binding is marked remote: true. The server itself starts without Cloudflare credentials; only a Genie call needs them, and without them it fails as GENIE_ERRORS.ASLEEP rather than stopping the run.
  • The three rate limiters, kept: not to limit anything, but because the routes behind joining by invitation code and the problem-report form call .limit(...) unconditionally and would fail with a TypeError without them.

Running Without a Cloudflare Account ​

npm run dev (and ./gradlew devNoGenie / devMock) starts the Worker on the local environment, which declares no Workers AI binding. ./gradlew dev is the one that does not: it runs local-genie, and wants the account below. That is deliberate: Workers AI has no local simulator, so the binding's mere presence makes Wrangler open a remote proxy session and ask you to log in to Cloudflare. With it declared, a fresh clone could not start the backend at all, the whole app held up by one optional feature.

The consequence is visible in exactly one place: the Article Genie button next to the market's search bar is not rendered. The backend reports the feature off on GET /api/v1/session and the frontend hides both the trigger and the panel, so there is no broken button to click. Everything else, leagues, the market, formations, scoring, behaves normally.

Turning the Genie on ​

Only needed if you are working on the Genie itself. It requires a Cloudflare account, and every question spends part of the day's shared neuron allocation.

bash
wrangler login          # once
cd backend && npm run devgenie

That runs the local-genie environment, local plus the ai binding. It uses the same local D1 data as local, so no re-migration is needed when switching between them.

See Article Genie LLM Integration for how the flag propagates from the binding to the button.

Creating a Cloudflare API token ​

wrangler login is the easy path natively, and the only thing that works where a browser does. It is not always available: in a container its OAuth callback listens on localhost:8976, which is the container's own loopback, so the consenting browser on the host has nothing to talk to. It is also the wrong shape for CI, which has no browser at all.

An API token covers both. Wrangler reads CLOUDFLARE_API_TOKEN from the environment and prefers it over any stored login session, so it needs no interaction anywhere.

1. Create it. Go to https://dash.cloudflare.com/profile/api-tokens → Create Token → Create Custom Token (the bottom option, the templates above it are all far broader than this needs), then add the least that works:

SectionGroupResourceLevel
AccountWorkers Scriptsyour accountEdit
AccountWorkers AIyour accountRead
AccountAccount Settingsyour accountRead

Workers AI · Read calls the model, and Account Settings · Read lets Wrangler resolve which account you mean. Leave Client IP Address Filtering empty and TTL at the default unless you have a reason.

Workers Scripts · Edit is the surprising one, and it is not optional. Because Workers AI has no local simulator, wrangler dev opens a remote preview session to carry the calls, and that machinery lives under Workers Scripts rather than under Workers AI. Without it Wrangler stops with Failed to establish remote session due to an authentication issue and the backend never starts, while wrangler whoami still succeeds, because the token is perfectly valid and merely unauthorised for that one endpoint (GET /accounts/<id>/workers/subdomain/edge-preview, error code 10000). WRANGLER_LOG=debug is what prints the endpoint; the plain error does not.

So this token is not read-only: Workers Scripts · Edit can publish a Worker to your account, and it will be sitting in a file on your laptop. It is still narrower than the deploy token CI uses, no Pages, no D1, but treat it as a write credential, and if that is not a trade you want, ./gradlew noGenie and npm run dev need no Cloudflare account at all.

2. Find your account id. It is the hex string in the dashboard URL (dash.cloudflare.com/<account id>/...), and also sits under Workers & Pages → Overview in the right-hand sidebar.

3. Put both where they are read from. wrangler.jsonc declares no account_id, so Wrangler asks the API, which means CLOUDFLARE_ACCOUNT_ID is required, not optional, as soon as your token can see more than one account.

  • Natively: export them in your shell, or leave wrangler login in place and skip the token entirely.

    bash
    export CLOUDFLARE_API_TOKEN=...
    export CLOUDFLARE_ACCOUNT_ID=...
  • In Docker: put both at the bottom of backend/.dev.vars, the same file the Worker's other secrets live in (.dev.vars.example carries the two names, commented with how to create the token). Compose passes that whole file into the container's environment, so nothing has to be exported there either.

    bash
    cp backend/.dev.vars.example backend/.dev.vars   # if you have none yet

    There is no .env in the repository root, on purpose, a third env file for two values is a worse trade than putting them beside the secrets that were already there. Note the direction of the split, though: Wrangler reads .dev.vars for the Worker's vars, never for its own credentials, so natively that file does not authenticate you. Only Compose turns it into the process environment Wrangler actually reads.

Then ./gradlew dev --parallel natively, or ./gradlew up in Docker. The backend prints ==> Article Genie on and the market grows its Genie button; without a token the Docker path stops with a message saying so rather than hanging on a login it cannot finish. Container specifics are in Running FantasyWiki in Docker.


How It Works ​

Browser
  │
  ├─ GET /auth/google  ──────────────────► Wrangler (127.0.0.1:8787)
  │                                              │ Google OAuth redirect
  │                                              │ Sets session_token cookie
  │                                              │ Redirects to localhost:5173/auth/callback
  │
  ├─ GET /auth/callback (Vue page)
  │      │
  │      └─ GET /api/v1/session ────────────► Wrangler (127.0.0.1:8787)
  │                                              │ Reads JWT from cookie
  │                                              │ Returns user info
  │
  └─ All other /api/* calls ─────────────► MSW (intercepted in browser)
                                                 Returns mock data from handlers.ts

The key insight: MSW uses passthrough() for /api/v1/session and /auth/*, so those requests reach the real Wrangler backend. Everything else is mocked.


Troubleshooting ​

ErrorCauseFix
ERR_SSL_PROTOCOL_ERRORVITE_BACKEND_URL missing http:// prefixAdd http:// explicitly in .env.local
redirect_uri_mismatchLocal URI not registered in Google ConsoleFollow Step 3 above
401 on /api/v1/sessionFRONTEND_URL in .dev.vars still points to productionCheck .dev.vars exists inside backend/ and restart Wrangler
Backend not reachableWrangler not running or wrong portRun npm run dev in backend/ and check the port in the log
Cookie not sentBrowser privacy settings blocking cookiesUse Chrome/Firefox, disable aggressive privacy extensions during dev
Wrangler asks you to log in to CloudflareYou started the local-genie env (or added an ai binding to local)Use npm run dev, the Genie is optional, see above
No Genie button in the marketExpected on npm run devNothing to fix; npm run devgenie if you need it
MongoServerSelectionError on npm run devmongoNo MongoDB listening, or it is a standalone rather than a replica setRun the two docker lines above; transactions need a replica set
Failed to start the remote proxy session / auth token has expiredSome bindings are proxied to the real Cloudflare, npm run dev binds Workflows, which needs a live token to start at allnpx wrangler login. npm run devmongo binds no Workflow and starts without it
The Genie answers ASLEEPThe Workers AI call failed, usually no valid Cloudflare tokennpx wrangler login; everything else works without one

Built from the repository's own documentation. Source on GitHub.