wfs-frontend
The website (and a barely started phone app) for World Fantasy Soccer. It is what players see: pick a team, make transfers, check leaderboards and mini-leagues, make predictions. Moderators and admins also use it to set up seasons and fix data.
Code: /home/rob/code/wfs-frontend ยท Last checked against commit 216b3e1 on 2026-10-14
WFS is its own game and is not related to Football Manager. Related pages: wfs-backend (the API this site calls), wfs-beta-frontend (an older copy).
Current status
The newest commits ("Close site", 29 July 2026) change the site so every page shows a footer banner saying "WFS has closed", with a link to a closing message at /closing-message. The message explains that WFS is shutting down after the World Cup, because running it takes too much of the owner's time. The rest of the app is still intact. Page code for the banner is in apps/web-vue/src/App.vue; to reopen, remove that footer and link.
What it does
A single-page website built with Vue. It has no data of its own: every list, score and button talks to the wfs-backend API. For the meaning of terms like gameweek, entry, chip and mini-league, see the "What it does" section of the backend page. In this repo:
apps/web-vue: the website. This is the part that is live.apps/mobile-expo: an Expo phone app that only has a start screen so far.packages/: code shared by both:types(data shapes),constants,api(the fetch client and a built-in fake backend),queries(cached data loading),utils, anddesign-tokens(colours and sizes).
Pages
Address patterns. A "season" is addressed by a short readable name (its slug) rather than a number.
| Address | What it shows |
|---|---|
/ | Home: all fantasy seasons, with tabs and search |
/auth | Log in, register and lost password on one page |
/settings | Account, your teams, email subscription, mini-leagues |
/reset-password/:token | Password reset link target |
/predictions, /predictions/me, /predictions/me/log | The predictions side game |
/competition/:id, /team/:id, /player/:id, /search | Real-world information pages |
/faq, /terms, /privacy, /closing-message | Static pages |
/s/:slug | A season: deadline countdown, fixtures, players, leaderboard |
/s/:slug/pick-team | Choose your squad, captain and chips |
/s/:slug/rules, /leaderboard, /entries, /gameweeks, /fixtures, /players, /teams, /price-changes, /predictions, /mini-leagues, /settings | Season sub-pages (add after /s/:slug) |
/s/:slug/entry/:entry and /s/:slug/entry-gameweek/:entry/:gameweek | Someone's team across the season, or in one gameweek |
/s/:slug/mod/... | Moderator tools for one season (players, teams, fixtures, setup, sources) |
/admin/... | Admin area: seasons, competitions, real-world teams/players/fixtures, gameweeks, chips, users, price changes, transfers |
Old /season/:id/... addresses from the previous version of the site are redirected to the new /s/:slug/... ones (apps/web-vue/src/router/legacyRedirects.ts). The full screen-by-screen description, with a done/partial status for each, is in agent/routes.md.
Running it
You need Node 22 or newer and pnpm 10 (turned on with corepack enable). Run everything from the repo root, not from apps/web-vue.
cd {{APP_DIR}}
corepack enable
pnpm install
Without a backend (fake data). The default. Opens at http://localhost:5173.
cd {{APP_DIR}}
pnpm dev:web
Against a real backend. Copy the example settings file and set the API address in it:
cd {{APP_DIR}}
cp apps/web-vue/.env.example apps/web-vue/.env
# then edit apps/web-vue/.env so that it contains:
# VITE_API_BASE_URL={{API_URL}}
There is also apps/web-vue/.env.development, which points at /api; the dev server forwards /api to a local backend with a self-signed certificate (see apps/web-vue/vite.config.ts). The phone app starts with pnpm dev:mobile.
Checks
cd {{APP_DIR}}
pnpm lint
pnpm typecheck
pnpm test
pnpm lint also runs a colour check (scripts/check-css-tokens.mjs) that complains about hard-coded colours that should come from the shared design tokens.
Deploying
The result of a build is a folder of plain files that nginx serves. There is no Node process running on the server. The first-time server setup (nginx block, HTTPS with certbot, permissions) is written out step by step in deploy.md, with ready-made nginx, Caddy, Vercel and Netlify snippets in apps/web-vue/deploy/.
Production build and update:
cd {{DEPLOY_DIR}}
git pull --ff-only
pnpm install --frozen-lockfile
pnpm build:web:prod
sudo chmod -R a+rX apps/web-vue/dist
Beta build instead (use pnpm build:web:beta in the same steps). The files to serve appear in apps/web-vue/dist; point nginx's root at that folder. A new build is picked up with no nginx reload, because the front page file is never cached.
Which API each build talks to is fixed at build time from a settings file:
apps/web-vue/.env.production:https://api.worldfantasysoccer.com/apiapps/web-vue/.env.beta:https://beta-api.worldfantasysoccer.com/apiapps/web-vue/.env.development:/api
(The deploy guide deploy.md still lists api2026.worldfantasysoccer.com as the production API; trust the settings files, which are what the build uses.)
Configuration
VITE_API_BASE_URL: the backend's API address, or the wordmockfor fake data. It is baked into the site when you build, so changing it needs a rebuild.VITE_APP_ENV:development,productionorbeta.
A production build that has no API address, or has mock, refuses to start and shows "Refusing to boot with the mock API client". That is on purpose so fake data can never reach real users.
Logging in uses a token the backend hands out, kept in the browser's local storage (the phone app uses its secure store). If the API is on a different address from the website, the backend must allow that website address, or every request fails.
Whatever serves the files must send unknown addresses to index.html, otherwise reloading a page like /s/some-season/pick-team shows a 404.
How it works
- Adding a new piece of data takes five steps: describe it in
packages/types, add a fetch function inpackages/api/src/fetchers.ts, add a fake answer inpackages/api/src/mock.ts, add a cached query inpackages/queries, then use it from a small helper inapps/web-vue/src/composables. - The shared packages are compiled first;
pnpm dev:weband the build commands do that for you. - Pages live in
apps/web-vue/src/pages(withadmin/andseason/sub-folders), reusable parts incomponents, routes inrouter/index.ts. - Draft squads are not changed in place on the page; changes are sent to the backend, which owns the rules (see the note in
agent/on not mutating drafts locally). - What the pages can show depends on what the API provides. Gaps are listed in
agent/backend-data-model-gaps.md(for example, the home page's nation, gender and tier filters are empty for that reason). Anopenapi.yamllink in the repo root points to the backend's copy and is broken on this server; ignore it. - Terms like "team" mean different things (a real club versus a user's squad); the repo's
agent/glossary.mdexplains them.
Gotchas
- Blank page after deploy. The API address was missing when the build ran; fix the settings file and rebuild.
- 404 on reload or 403 from nginx. Missing the fall-back to
index.html, or a parent folder that nginx cannot enter. Do not deploy from a home folder; use/srvor/var/www. - Build killed with no message. The build needs about 1.5 GB of memory; add swap on a small server.
- Serve
dist, notapps/web-vue. The sourceindex.htmlis only a template for development. - The site is currently marked as closed. See Current status.
- The repo contains many human-testing spreadsheets (
*-human-testing.csv,testmo-import.csv) used to write manual test cases; they are not part of the app.
Changelog
- Initial version.