Environment variables

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-app check-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. Yes
CONTENTFUL_ENVIRONMENT club.config.ts (env.server, default develop); sync-webhooks, sync-preview-urls (after CONTENTFUL_MODEL_ENV) The Contentful environment the SITE reads. Staging and production set their own. No
CONTENTFUL_ACCESS_TOKEN club.config.ts (env.server); CI secret The Content Delivery API token. Yes
CONTENTFUL_PREVIEW_TOKEN club.config.ts (env.server, default empty) The Content Preview API token. Empty = draft preview is off. No
CONTENTFUL_PREVIEW_SECRET apps/web/src/app/api/preview/route.ts; packages/model/scripts/sync-preview-urls.mjs 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. No
CONTENTFUL_WEBHOOK_SECRET apps/web/src/app/api/revalidate/route.ts; packages/model/scripts/sync-webhooks.mjs (required there) 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. No

Competition Engine and the basketball data areas

Variable Read where Purpose Required
COMPETITION_ENGINE_BASE_URL club.config.ts (env.server, default http://localhost:8080); packages/competition-engine/scripts/record-fixtures.mjs 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. With the switch
NEXT_PUBLIC_ELTV_WATCH_URL club.config.ts; packages/basketball-app/src/games/game-centre.tsx 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. No

Detail: Analytics.

Variable Read where Purpose Required
NEXT_PUBLIC_GTM_CONTAINER_ID club.config.ts (env.public) 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. No

Caching (Redis and the Cloudflare edge)

Detail: Caching.

Variable Read where Purpose Required
CACHE_REDIS_URL 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.ts identity.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). No
DOCS_PASSWORD apps/docs/src/proxy.ts, apps/docs/src/lib/access.ts 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. No
Workflow-internal variables scripts/ci/*.mjs DEPLOY_ENVIRONMENT, DEPLOY_LOG_URL, DEPLOY_SHA, DEPLOY_SITE_URL, DEPLOY_STATE, PREVIOUS_STATE, MODEL_ENV, APPLY_LOG, APPLY_OUTCOME, DRIFT_DIFF, HEARTBEAT, PR_NUMBER, PR_TITLE, PR_BRANCH, PR_LABELS, MATRIX_RESULT, STORY_RESULT, EXPECTED_SHARDS, RESULTS_DIR, PASS_RATE_FLOOR, FAILED_STEPS, RELEASE_READY_STATUS, RELEASE_STAGED_STATUS, RELEASE_QA_PASSED_STATUSES, JIRA_API_BASE_URL and GitHub's own GITHUB_*. Set by tooling

Contentful model tooling (@v4/model)

The model scripts also read CONTENTFUL_SPACE_ID (above) and refuse to run without it.

Variable Read where Purpose Required
CONTENTFUL_CMA_TOKEN sync-schema, migrate, sync-apps, sync-dictionary, push-dictionary, add-locale, seed, sync-webhooks, sync-preview-urls, migrate-text-tags, the player clean-up scripts; content-app check-addresses; @v4/tickets build-cms-model --from=contentful; CI secret The single Contentful management secret. sync-schema derives environment-scoped delivery keys from it, so CI carries only this one. Yes, for model work
CONTENTFUL_MODEL_ENV sync-schema, sync-apps, sync-dictionary, add-locale, sync-webhooks, sync-preview-urls The environment the model scripts target (default develop). migrate takes --env explicitly instead. No
ALLOW_PROTECTED_ENV migrate, sync-apps, sync-dictionary, add-locale, seed, migrate-text-tags and the player clean-up scripts Lets CI pass the protected-environment guard (master, production, staging, staging-release). Never set it locally. Set by tooling (CI)
LOAD_BALANCER_URL, SITE_BASE_URL sync-webhooks, sync-preview-urls The site the webhook and preview URLs point at, in that order of preference, then NEXT_PUBLIC_SITE_BASE_URL. No
CONTENTFUL_ORG_ID, CONTENTFUL_APP_CHANNEL, CONTENTFUL_HOSTED_APP_SLUGS 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 The Jira API token. No
JIRA_BASE_URL the ticket and CI scripts Overrides tickets.config.json jira.site. No
JIRA_PROJECT_KEY register-epics, scripts/ci/pr-ticket-check.mjs, release-jira.mjs Overrides the board for ONE run. The board is config (jira.defaultProject); never export this. No
STORYBOOK_BASE packages/tickets/scripts/build-storybook-map.mjs The Storybook origin the story map is built from (default publishing-hub-storybook.vercel.app, the template's hosted Storybook, not this repo's). No

Local tooling and tests

Variable Read where Purpose Required
DOCS_DRIFT scripts/hooks/docs-drift.mjs Unset = the pre-commit check warns and never blocks. block = staged drift fails the commit, as in CI. off = skip the check. No
E2E_PORT apps/e2e/playwright.config.ts The port the e2e serves Storybook on (default 6006). Use a free one when another checkout's Storybook holds 6006. No
E2E_FULL_MATRIX apps/e2e/playwright.config.ts; the nightly e2e workflow 1 runs the full spec set on WebKit and Firefox, not just the cross-browser subset. No
E2E_DOCS_SWEEP apps/e2e/tests/shared/docs-pages-render.e2e.ts Opts in to the sweep that renders every docs page. No
BASKETBALL_APP_URL apps/e2e/tests/basketball-app/data-areas.e2e.ts A running web app to test the basketball data areas against. Unset = the spec skips. No
BASE, RUNGS, CONTRAST scripts/page-audit.mjs, scripts/clip-sweep.mjs The app to sweep (default http://localhost:3000), the viewport widths, and CONTRAST=1 to list every pair below WCAG AA. No
BASE_URL scripts/loadtest/eviction-storm.mjs The load test's target. No