Every environment variable this repo reads: where it is read, what it does and whether it is required, grouped by feature.
2026-11-07
Every variable this repo reads, grouped by what needs it. Copy the root .env.example to
.env (gitignored) and fill in only what your task needs: each variable is needed only by the
tooling or feature that names it.
This page is the full list; .env.example is not. The example file leaves out
CONTENTFUL_ENVIRONMENT, CONTENTFUL_PREVIEW_TOKEN, CONTENTFUL_PREVIEW_SECRET and the two
CLOUDFLARE_* variables, so do not treat it as the contract. When you add a variable, add it
here and to .env.example in the same change.
How to read the Required column:
Yes — the build fails without it. Variables read through club.config.ts's
env.public() / env.server() markers fail the build naming the missing variable.
Deployed — local development has a working default, but every deployed environment must
set its own value.
With the switch — required only once the feature's switch variable is set.
No — optional; unset turns the feature off or uses the stated default.
Set by tooling — written by a script, the build or the platform. Never set it by hand.
NEXT_PUBLIC_* values ship in the browser bundle, so they must never hold a secret.
Contentful (the site)
Read by apps/web/club.config.ts unless stated otherwise.
Variable
Read where
Purpose
Required
CONTENTFUL_SPACE_ID
club.config.ts (env.server); the model scripts; content-appcheck-addresses; CI's repository variable
The Contentful space: 12gd2cvfgb6b ("Euro League"). Never defaulted — the model scripts refuse to run without it, so nothing can fall back to another space.
The shared secret /api/preview checks before it turns on draft mode, and which sync-preview-urls writes into the space's preview URLs. Set it together with the preview token.
The x-revalidate-secret the Contentful publish webhook — and the Competition Engine's post-ingest call — sends to /api/revalidate. Unset = the route answers 503; a wrong secret is refused.
Deployed
NEXT_PUBLIC_SITE_BASE_URL
club.config.ts (env.public, default http://localhost:3000); the fallback target of sync-webhooks / sync-preview-urls
The site's public origin. Every canonical, og:url, JSON-LD id, sitemap entry and share-image address is made absolute against it. The build fails when crawling is allowed on a local address.
Deployed
NEXT_PUBLIC_ROBOTS_ALLOW
club.config.ts
Crawling is allowed only when this is exactly true. Production only — dev and staging must never be indexed.
The Competition Engine's origin. The default is a local stadion-euroleague instance, so a deployed environment must set its hosted one.
Deployed
BASKETBALL_ROUTES
apps/web/src/lib/basketball-routes.ts (used by the middleware, the header's crest strip, the sitemaps and the article and home hero widgets)
false turns the basketball domain dark: every data-area page (teams, players, coaches, games, standings, stats) and its API routes answer 404, the sitemap leaves them out and the crest strip is not drawn. Unset or true = on. The header and sitemap are cached at build, so set it in the environment the site is BUILT in as well as the one it runs in.
No
GAME_CENTRE_REPLAY
packages/basketball-app/src/games/live-route.ts
on lets the live route play a finished game back as if it were live (?replay=<speed>&replayFrom=<ms>). A development aid — never set it on production.
No
COMPETITION_ENGINE_REPO_DIR
packages/competition-engine/scripts/sync-spec.mjs
A local clone of stadionHQ/stadion-euroleague that sync-spec vendors the OpenAPI spec from when GitHub is unreachable (default ../stadion-euroleague, beside this repo).
No
Video and media
Variable
Read where
Purpose
Required
NEXT_PUBLIC_VIDEO_API_BASE_URL
club.config.ts (env.public, no default)
The Stadion video platform API that @v4/media reads. Euro League bought the video domain, so every build needs it.
Yes
NEXT_PUBLIC_BRIGHTCOVE_ACCOUNT_ID
club.config.ts (env.public, no default)
The Brightcove account the player uses.
Yes
ELTV livestream
NEXT_PUBLIC_VESPER_API_KEY is the switch. Detail:
ELTV livestream.
Variable
Read where
Purpose
Required
NEXT_PUBLIC_VESPER_API_KEY
club.config.ts (env.public)
The Vesper Web SDK key — public by nature (the browser sends it as x-api-key). One per environment, in 1Password. Unset or empty = no player renders and the SDK is never downloaded.
No
NEXT_PUBLIC_VESPER_BASE_URL
club.config.ts (env.public)
The Vesper front-office origin, with no path or trailing slash: https://euroleague.api.onvesper-staging.com or https://euroleague.api.onvesper.com.
With the switch
NEXT_PUBLIC_VESPER_ENVIRONMENT
club.config.ts (env.public, no default)
stag or prod, matching the key — a staging key must never run as production.
The ELTV site every link-out falls back to (e.g. https://euroleague.tv), and the Game Centre's watch link.
With the switch
NEXT_PUBLIC_ELTV_LOGIN_URL
club.config.ts (env.public)
The page the player's "Log in to watch" link opens; may contain {returnTo}. Unset = no log-in action is offered.
No
NEXT_PUBLIC_ELTV_ACCESS_URL
club.config.ts (env.public)
The livestream access endpoint, a URL template containing {gameCode}. Unset = the player renders nothing.
No
WSC Stories and Moments
Variable
Read where
Purpose
Required
NEXT_PUBLIC_WSC_API_KEY
club.config.ts (env.public)
The switch. Public by nature (the SDK writes it into the page); WSC issues separate Dev and Prod keys (in 1Password). Unset or empty = no WSC row renders.
No
NEXT_PUBLIC_WSC_ENVIRONMENT
club.config.ts (env.public, default prod)
The WSC stage the key belongs to: prod or uat. Leave it unset rather than empty: once a key is set, an empty value fails the build.
No
ECAL "Add to calendar"
Variable
Read where
Purpose
Required
NEXT_PUBLIC_ECAL_API_KEY
club.config.ts (env.public)
The switch. Public by nature (ECAL's embed code puts it on the script tag). Unset or empty = no "Add to calendar" button and no ECAL script. Each competition's display (widget id and category) is a literal in club.config.ts.
ELB's Google Tag Manager container for the environment (GTM-XXXXXXX). An id of another shape fails the build. Unset = no container loads.
No
NEXT_PUBLIC_ONETRUST_DOMAIN_SCRIPT_ID
club.config.ts (env.public), checked against the pattern in packages/analytics/src/settings.ts
OneTrust's domain script id: a UUID, with -test for its test script off production. Unset = no cookie banner — and then no event is ever sent, because every event waits for consent.
No
NEXT_PUBLIC_WSC_TRACKING_PREVIEW
apps/web/src/lib/analytics.ts
true on Dev only, never production: a tester may grant consent by hand (localStorage["wsc:tracking-consent"] = "granted") where OneTrust is not on the page, to check events in the dataLayer and GTM's preview. Every local dev server allows it without this.
No
Single sign-on (Cortex)
CORTEX_SSO_BASE_URL is the switch, read by packages/cortex-sso/src/settings.ts through
club.config.ts. Once it is set, the variables marked "With the switch" are required at build
and at runtime, and a missing one throws naming it. Detail:
Single sign-on.
Variable
Read where
Purpose
Required
CORTEX_SSO_BASE_URL
cortex-sso settings
The switch. Unset = sign-on is off, the header has no Login control and /api/auth/* answers 404. Stage https://oauth-stage.fanscore.com, production https://oauth.fanscore.com.
No
CORTEX_MICROSITE_URL
cortex-sso settings
The hosted login and account site.
With the switch
CORTEX_SSO_CLIENT_ID
cortex-sso settings
EUROLEAGUE. The client is public (PKCE), so there is no client secret.
With the switch
CORTEX_SSO_ISSUER
cortex-sso settings
The iss in Cortex's ID tokens, compared character for character.
With the switch
NEXTAUTH_URL
cortex-sso settings and NextAuth
This site's public address; the callback and logout-return addresses registered with Cortex are built from it. Local: http://localhost:3000.
With the switch
NEXTAUTH_SECRET
NextAuth (cortex-sso/src/options.ts)
Secret. Encrypts the session cookie. Random and unique per environment (openssl rand -hex 32); changing it signs everyone out.
With the switch
AUTH_SESSION_MAX_AGE_SECONDS
cortex-sso settings
How long a login lasts, counted from the login. Default 86400; anything but a whole number above zero fails.
apps/web/use-cache-handler.mjs at runtime; club.config.ts declares it only when set
The per-client Redis that shares tag invalidation across instances. Unset = each instance keeps its own cache. A Vercel deployment leaves it unset and uses the platform's cache.
No
CLOUDFLARE_API_TOKEN
apps/web/src/lib/cloudflare-purge.ts
A token with Zone → Cache Purge permission. With CLOUDFLARE_ZONE_ID, /api/revalidate also purges the Cloudflare edge on publish, so a publish is fresh there in seconds rather than after s-maxage. Set it only when the client caches HTML at the edge. Unset = no edge purge.
No
CLOUDFLARE_ZONE_ID
apps/web/src/lib/cloudflare-purge.ts
The zone the purge targets. Pairs with the token.
No
Build and runtime flags
Variable
Read where
Purpose
Required
PH_THEME_EMISSION
set to club by apps/web/next.config.ts; read by createPackageTheme
Makes the club theme the app's single emitter: the packages only register their themes. Storybook and the e2e never set it.
Set by tooling
CACHE_PREFIX
set by apps/web/next.config.ts from club.config.tsidentity.name
A build-known prefix kept as defence in depth. The cache handler deliberately ignores it, because the site has its own Redis.
Set by tooling
VERCEL
apps/web/next.config.ts
Set by the platform. On Vercel the app uses the native cache; anywhere else it registers the Redis-aware cache handler.
Set by tooling
NODE_ENV, CI
various
Set by Node, Next and CI. NODE_ENV also decides whether a local dev server allows hand-granted consent.
Set by tooling
Install and CI
Variable
Read where
Purpose
Required
NPM_TOKEN
.npmrc; every CI install (GitHub secret)
The @stadionhq-v3/* video SDK packages and @dicetechnology/vesper-sdk-web are private on registry.npmjs.org, and install 404s without auth. Use npm login (as an identity granted the Vesper package) or a read-only granular token that reads both.
Yes, for install
ANTHROPIC_API_KEY
scripts/hooks/docs-drift.mjs in CI; apps/docs/src/lib/chat.ts
Turns on the docs-drift check in CI, and is the server key for the docs app's chat assistant — set it on Flightcontrol's euro-league-docs-web-development service (a reader may supply their own instead).
The password the docs ask for — set it on Flightcontrol's euro-league-docs-web-development service. Every page and API then needs the sign-in cookie; unset, the docs are open.
No
SLACK_WEBHOOK_URL, SLACK_RELEASE_WEBHOOK_URL
scripts/ci/slack-post.mjs; the deploy-watch and release workflows (GitHub secrets)
Slack notifications from CI.
No
CONTENTFUL_APPS_TOKENS
packages/model/scripts/release-apps.mjs
Per-organisation tokens that release the Contentful app bundles.
set by migrate.mjs, read by scripts/app-widget.cjs
Resolve an app binding by slug, for the organisation and release channel being migrated.
Set by tooling
Design tooling (Figma)
Variable
Read where
Purpose
Required
FIGMA_PAT
packages/token-parser/scripts/* (extract-*, design-drift, design-status, fork-diff, the screenshot exporters); tokens-euro-league's colour check; the ticket scripts (match-figma, sync-screenshots, sync-figma-frames); the Figma MCP (passed as FIGMA_API_KEY) and the Figma skills
A personal access token with file-read scope (Figma → Settings → Security). check-bridges is offline and does not need it.
No
STORYBOOK_FIGMA_ACCESS_TOKEN
@storybook/addon-designs only — no repo code reads it
The Storybook design panel. The same token as FIGMA_PAT works.
No
Tickets (@v4/tickets)
Variable
Read where
Purpose
Required
JIRA_EMAIL
the ticket scripts; scripts/ci/jira-test-comment.mjs, release-jira.mjs
The Atlassian account email. Falls back to git config user.email.
No
JIRA_API_TOKEN (or ATLASSIAN_API_TOKEN)
push-jira, adopt-board, write-panels, sync-screenshots, tickets-doctor and the other board scripts