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:
| File | Location | Purpose |
|---|---|---|
backend/.dev.vars | backend/ | Secrets for Wrangler (backend) |
frontend/.env.local | frontend/ | 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:
cp backend/.dev.vars.example backend/.dev.vars
cp frontend/.env.local.example frontend/.env.localThen 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:
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_IDis 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 inwrangler.jsonc).FRONTEND_URLis passed as a--varat deploy time; locally it must be set here too.JWT_SECRETcan be any random string. Generate one with:bashnode -e "console.log(require('crypto').randomBytes(32).toString('hex'))"GOOGLE_CLIENT_SECRETis 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: setVITE_DEV_LOGIN=truein Step 2 and sign in as the demo player instead.GH_APP_PRIVATE_KEYbacks the in-app problem report form (/report), which opens an issue onFantasyWiki/FantasyWikiasFantasyWiki[bot]. Leave it unset unless you are working on that form, every submission with a valid key files a real issue (labelledpreviewoutside 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
\nso 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_REPOandENVIRONMENTare plain vars and already live inwrangler.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:
VITE_BACKEND_URL=http://127.0.0.1:8787
VITE_MOCK=true
# VITE_DEV_LOGIN=true
# VITE_PASSWORD_AUTH=trueNotes:
VITE_BACKEND_URLis used as-is by the frontend runtime, so includehttp://explicitly for local backend.VITE_MOCK=trueenables MSW (Mock Service Worker), which intercepts all API calls except/api/v1/sessionand/auth/*, which pass through to the real local backend.VITE_DEV_LOGIN=trueputs a Continue as demo player button on the login screen, which mints the same session Google would without going near Google, so you can leaveGOOGLE_CLIENT_SECRETblank and skip Step 3 entirely. The button only exists in a build started with the variable set, and the backend answers/auth/devwith 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=trueputs a username and password form on the login screen. It works only against a backend started withnpm run devmongo(or./gradlew devMongo): username/password sign-in lives insrc/indexPassword.ts, which onlywrangler.mongo.jsoncnames, so the D1 local run, and every deployment, does not contain those routes at all. Never set it in.env.previewor.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.
- Go to Google Cloud Console → Credentials
- Open the OAuth 2.0 Client ID for FantasyWiki
- Under Authorized redirect URIs, add:
http://127.0.0.1:8787/auth/google - Save and wait ~1 minute for propagation.
Step 4, Start the App
Open two terminals:
# Terminal 1, backend
cd backend
npm run dev # wrangler dev --env local
# Should print: Ready on http://127.0.0.1:8787# Terminal 2, frontend
cd frontend
npm run dev
# Should print: Local: http://localhost:5173/Or from the project root using Gradle:
./gradlew devMockWait 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:
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/:
npm run devmongo # wrangler dev --config wrangler.mongo.jsoncOr, with the frontend beside it, from the project root:
./gradlew devMongoThere 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 markedremote: true. The server itself starts without Cloudflare credentials; only a Genie call needs them, and without them it fails asGENIE_ERRORS.ASLEEPrather 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.
wrangler login # once
cd backend && npm run devgenieThat 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:
| Section | Group | Resource | Level |
|---|---|---|---|
| Account | Workers Scripts | your account | Edit |
| Account | Workers AI | your account | Read |
| Account | Account Settings | your account | Read |
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 loginin place and skip the token entirely.bashexport 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.examplecarries 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.bashcp backend/.dev.vars.example backend/.dev.vars # if you have none yetThere is no
.envin 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.varsfor 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.tsThe key insight: MSW uses passthrough() for /api/v1/session and /auth/*, so those requests reach the real Wrangler backend. Everything else is mocked.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
ERR_SSL_PROTOCOL_ERROR | VITE_BACKEND_URL missing http:// prefix | Add http:// explicitly in .env.local |
redirect_uri_mismatch | Local URI not registered in Google Console | Follow Step 3 above |
401 on /api/v1/session | FRONTEND_URL in .dev.vars still points to production | Check .dev.vars exists inside backend/ and restart Wrangler |
| Backend not reachable | Wrangler not running or wrong port | Run npm run dev in backend/ and check the port in the log |
| Cookie not sent | Browser privacy settings blocking cookies | Use Chrome/Firefox, disable aggressive privacy extensions during dev |
| Wrangler asks you to log in to Cloudflare | You 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 market | Expected on npm run dev | Nothing to fix; npm run devgenie if you need it |
MongoServerSelectionError on npm run devmongo | No MongoDB listening, or it is a standalone rather than a replica set | Run the two docker lines above; transactions need a replica set |
Failed to start the remote proxy session / auth token has expired | Some bindings are proxied to the real Cloudflare, npm run dev binds Workflows, which needs a live token to start at all | npx wrangler login. npm run devmongo binds no Workflow and starts without it |
The Genie answers ASLEEP | The Workers AI call failed, usually no valid Cloudflare token | npx wrangler login; everything else works without one |
Related
- Deploy Strategy & Branch Policy
- Persistence Targets
- NPM Script Naming Convention
- Article Genie LLM Integration
- Running FantasyWiki in Docker
- Frontend Testing: the same MSW handlers, driven by vitest
