StatsHub Docs
Status

Production replacement parity

Release status for replacing the legacy StatsHub website and API with statshub-web and statshub-api.

Last checked: 2026-08-30.

Production replacement is not approved

The replacement has complete source-level page and API route coverage, but it does not yet have enough authenticated, mutation, and operational evidence for a client production cutover. The open gates below must pass before switching traffic away from apps/web-legacy.

Evidence levels

LevelMeaning
Runtime verifiedExercised against a running replacement stack with the expected result.
Source verifiedThe legacy behavior and replacement implementation were traced, but the complete user flow has not run.
PartialThe implementation exists, but coverage or behavior remains unverified.
FailA confirmed release blocker remains.

File counts are inventory evidence, not behavior evidence. A copied page can still fail because routing, cookies, upstream data, or deployment settings differ.

Route and implementation coverage

SurfaceCurrent evidenceStatus
Customer page routesAll 73 legacy non-error, non-API page routes resolve at the same canonical path under apps/statshub-web/src/app. Public documents may live in the (public-static) route group; personalized documents remain in (app). The replacement also adds /login as an alias for /auth/login.Source verified
Page implementationsAll 73 customer page source files have a matching file under src/composed/main-ui/pages.Source verified
Shared componentsAll 312 legacy component paths have a matching file under src/components/main-ui.Source verified
React contextsAll 8 legacy context paths have a matching file under src/lib-client/legacy/contexts.Source verified
Legacy API routesThe generated Go port ledger records 218 of 218 routes as ported.Source verified
API contract casesThe suite contains 1,365 cases. A production-mode live comparison ran 1,357 cases after excluding the deliberately unsupported unbounded /api/player/all request: 1,100 matched exactly and all 218 ported routes remained represented. The 257 residuals were 251 generated error/auth/cache-policy differences and six hand-written auth/cache-context differences. None of the 19 generated happy-path residuals exposed a public data-response mismatch.Runtime verified
API routes with a contract caseAll 218 ported routes have at least one case. Generated probes now reuse stable hand-written successful requests, including required query parameters, rather than treating invalid bare URLs as happy paths. Authenticated success paths, mutation bodies, and important query combinations are still incomplete.Runtime verified

Customer capability matrix

CapabilityLegacy contractReplacement evidenceStatus
Guest browsingMost pages and tools are public. PropHunter limits guests to four blurred results.The copied page and component gates match the legacy source, including the four-result PropHunter slice.Source verified
Email OTP and Google sign-inLogin supports email OTP and Google OAuth.UI logic is copied. The Go callback now returns proxied development sign-ins to the replacement origin and has regression tests. The read-only provider audit returned HTTP 200 and confirmed that email and Google are enabled. Signed-in users now leave /login, /auth/login, and their localized forms through private, no-store redirects. A real provider login has not completed in the release environment.Partial
Session and route policySupabase session handling, login redirects, locale routing, and legacy protected-route checks run before page rendering.Canonical routes now execute the copied main-UI session policy; the experimental UI retains its separate policy. Focused route-policy tests cover both login aliases, localized redirects, signed-out access, refreshed cookies, and private redirect headers.Runtime verified
Free-account featuresAccount, favourites, watchlists, saved filters, and bookmaker preferences require login, not payment.All ten favourite team/player operations and all 16 saved-filter, watchlist, tracker-setting, and tracked-bet handlers require a validated session. A single disposable-PostgreSQL matrix now passes team/player favourite and notes, both watchlists, saved filters, bookmaker preferences, tracker settings, and tracked bets through create/reload or check/update/delete with second-account isolation. The signed-in browser matrix remains open.Partial
Paid toolsValue Bets v2, Bet Builder, Team Models, and Super Sub Heroes require an active or trialing entitlement.Client, server-side web, and Go API gates share the same active/trialing rule and deny paused subscriptions. Super Sub Heroes now rejects anonymous requests before cache or database access; Value Bets and Team Models retain their paid Telegram caller paths. Eight web persona cases, seven Go status cases, request-level Super Sub tests, and both full suites pass. An isolated updated API returned 401 from the Super Sub route; signed-in browser personas have not run end to end.Partial
Top-tier controlsStored tier pro unlocks raw model outputs and advanced controls; starter does not.Both Value Bets implementations now share one entitlement rule: active and trialing Pro accounts pass; Starter, paused Pro, expired Pro, and guests fail; superadmins retain inspection access. Eight deterministic tier-persona cases and the full web suite pass. A signed-in browser run remains open.Partial
Billing and account managementPricing creates checkout; account opens the portal and supports pause, resume, discount, and cancellation flows.Stripe routes are ported and account UI is copied. The authenticated handler matrix rejects unapproved prices; verifies new checkout metadata, existing-subscriber plan changes, and portal return URLs; and passes pause/resume, retention discount, and cancellation Stripe fields with PostgreSQL state synchronization. Webhook insert/update/delete propagation also passes. A real Stripe-hosted checkout and portal run remains open.Partial
Telegram and alertsPaid subscribers can link Telegram and activate eligible foul-card and lineup alerts.Telegram webhook updates require the configured standard secret-token header and fail closed when production omits it. Disposable-PostgreSQL lifecycles now pass lineup alert create/re-arm/list/remove with second-account isolation, plus foul/card default preferences, update, muted-league reload, disable, and paused-account denial. Linking and actual message delivery still occur through the external bots and have not run end to end.Partial
Admin and operator accessBot routing, broadcasts, ingestion, league configuration, basketball imports, Telegram rules, prompt templates, alert history, operational logs, reports, and bulk exports are restricted to superadmins in production.All 45 registered dashboard operations and all five admin operations now use the same production superadmin guard. Request matrices reject anonymous and signed-in-reader personas across the newly hardened handlers, the guard admits a deterministic superadmin, the generated OpenAPI contract includes 401/403, and development retains its configured open-console bypass. A signed-in superadmin browser run remains open.Partial
Spanish and PortugueseLocale choice persists and localized URLs reload in the selected language.Cookie redirect, localized rewrite, session-cookie preservation, and hydration are covered. A browser run reached /es/pricing with html.lang, provider state, cookie, and local storage all set to es, with no console errors.Runtime verified
Basketball subdomainbasketball.statshub.com rewrites football-shaped paths into the basketball route tree while excluding auth and API paths.Root and nested host rewrites now match legacy, with exclusions for already-prefixed basketball, auth, user, chat, and basketball API paths. A production-style host-header request rewrote / to /basketball and returned 200.Runtime verified
SEO and public contentEntity pages, blog, academy, metadata, robots, and sitemaps remain reachable.Main-UI canonicals, en/es/pt/x-default alternates, and the document language render in the initial server HTML; /en/... redirects to the unprefixed English URL. The production audit checked seven sitemaps, 83,573 unique canonical URLs, and 120 deterministic localized page samples with no failures.Runtime verified

External integration state

The read-only integration audit ran against the intended https://www.statshub.com replacement origin in Stripe test mode. It does not create users, prices, webhooks, bot registrations, or Sentry projects.

IntegrationEvidenceStatus
Supabase Auth/auth/v1/settings returned HTTP 200 with email and Google enabled.Pass
StripeBoth configured monthly test prices are active and in the expected mode. No enabled webhook targets /api/stripe/webhook with checkout-completed and subscription update/delete events.Fail
TelegramThe bot token, bot identity, webhook URL, pending-update count, and delivery state pass. TELEGRAM_WEBHOOK_SECRET is absent, so the API cannot validate the secret header expected from Telegram.Fail
SentryThe public DSN is missing or invalid, source-map upload is disabled, and the configured API token receives HTTP 401 from the project API.Fail

Run the same non-mutating check with:

node --env-file=.env \
  --env-file=apps/statshub-web/.env.local \
  --env-file=apps/statshub-api/.env \
  scripts/audit-production-integrations.mjs \
  --expected-origin=https://www.statshub.com \
  --stripe-mode=test

Authenticated persona smoke

The authenticated release gate consumes existing quality-assurance sessions; it never creates users, changes subscriptions, or prints tokens. Put one Supabase session object in each of these mode-600 files:

free.json
starter.json
pro.json
trialing.json
paused.json
expired.json
superadmin.json

Each file may contain the session directly or as { "session": { ... } }. Run the complete matrix from the web app:

cd apps/statshub-web
bun run smoke:personas -- \
  --web-origin=https://candidate.example.com \
  --sessions-dir=/secure/path/to/sessions

For each persona, the gate checks the subscription payload, personal watchlist read, paid-API access, login and localized-login redirects, superadmin routing, account-page hydration, the displayed plan state, the Raw Model Outputs Pro badge, browser exceptions, console errors, and same-origin request failures. The helper contract passes locally. The release run remains open because this workspace has no reusable QA sessions.

Release gates

GateRequired resultCurrent state
Auth personasGuest, free, starter, pro, trialing, paused, expired, and superadmin accounts produce the expected UI and API access.Partial: deterministic entitlement and authorization cases pass; the fail-closed seven-person browser/API runner is ready, but no reusable QA sessions are configured for its release run
Personal-data mutationsFavourites, notes, watchlists, saved filters, bookmaker preferences, tracker settings, and tracked bets survive reload and remain isolated per account.Partial: every named Go handler/database lifecycle passes against disposable PostgreSQL with second-account isolation; signed-in browser reload interactions remain open
Billing lifecycleCheckout, webhook, entitlement refresh, portal, pause, resume, retention discount, and cancellation pass in Stripe test mode.Fail: handler and persistence contracts pass, but Stripe has no enabled replacement webhook; hosted checkout and portal browser flows remain open
Telegram lifecycleLink, eligibility, enable, delivery, disable, and unlink pass with a test account and chat.Fail: database lifecycles pass and the registered bot webhook is healthy, but TELEGRAM_WEBHOOK_SECRET is absent; external link, delivery, and unlink remain open
Basketball host routingRoot and nested basketball subdomain URLs match legacy, including excluded auth and API paths.Pass
API contract depthEvery client-used route has a body/shape case; authenticated reads, mutations, and important query combinations have cases.Partial: production-mode live comparison covered 218 of 218 routes and 1,357 cases; authenticated success, mutation, and query-combination depth remains open
Web regression suiteThe full web test command passes.Pass: 1,029 tests across 63 files
Web lintESLint evaluates the source and the complete lint script passes.Pass: ESLint completes with 0 errors and check:caps reports no uppercase text transforms
Production buildNext.js compiles, typechecks, prerenders, and prepares the standalone server without exhausting a developer workstation.Pass: the default entity warm set is bounded to 496 generated pages instead of the previous estimated 15,872-page fan-out; the verified build emitted 958 total pages and completed in about 25 seconds
SEO crawlSitemaps obey protocol limits and contain only canonical 200 pages; sampled pages emit the matching canonical, hreflang set, title, index policy, and one H1.Pass: seven sitemaps, 83,573 unique URLs, and 120 deterministic localized page samples completed with no failures
Serving capacityThe replacement meets the agreed latency, throughput, response-size, and memory envelope at expected client load.Partial: 100 Club and Player Stats now emit reusable five-minute public cache policy and reach about 323 and 338 origin requests per second, narrowing the prior gap to roughly 2.4×–3.3×; fresh uncapped web containers settle near 157 MiB after cooldown versus 167 MiB for legacy, but the edge-backed release image still needs its deployment smoke
Production monitoringSentry receives browser/server events and release source maps resolve stack traces.Fail: the DSN is missing or invalid, source-map upload is disabled, and project API access returns HTTP 401
Production smoke and rollbackCritical routes pass on the release image, monitoring is live, and the legacy deployment remains a tested rollback target.Partial: a clean committed-HEAD API candidate plus the production-built web passed all 27 guest/API/SEO/host/rollback checks, including paid-tool denial, and all seven legacy rollback documents are healthy; the seven-person authenticated runner is ready, but the deployed preview, QA sessions, and monitoring remain open

Verified fixes in this audit

CommitResult
d220cb6eOAuth callbacks honor the forwarded replacement host in development while retaining the direct legacy fallback.
4387364bCanonical routes run the copied main-UI session and protected-route policy.
06d6d851ES/PT selection survives redirect, internal rewrite, session-cookie refresh, and client hydration.
d1486419Basketball subdomain requests rewrite into the basketball route tree while preserving legacy exclusions.
a6d9e2a9Production header tests protect personalized APIs without disabling the public route cache.
b130f23aThe experimental bet builder uses the title-case Corners translation expected by its market model.
1756a720One-character player-name typos reach fuzzy matching instead of being mislabeled as abbreviations.
65dce665Assistant comparisons explicitly parallelize independent lookups within one topic.
613656acWeb lint resolves a supported TypeScript compiler instead of crashing inside ts-api-utils.
4342fdc8ESLint completes its full source scan with no blocking errors.
bb28ebd3Paused subscriptions are denied consistently by both web shells, server-side web guards, and Go premium endpoints.
cc95644bSuper Sub Heroes rejects anonymous Go API requests before cache or database access, matching the legacy premium gate.
94ce4bcbFavourite reads, writes, note edits, and checks require the authenticated owner instead of trusting a caller-supplied user id.
10ba304eBot routing changes and Telegram broadcasts require a production superadmin.
519690ddNineteen Telegram-dashboard reads and controls require a production superadmin.
670fc44dOperational logs, stale-odds data, daily reports, and bulk league exports require a production superadmin.
7c6c4320Both MCP transports enforce MCP_API_KEY for JSON-RPC requests when it is configured.
8feafa15Telegram webhook updates require the registered secret-token header and fail closed when production is not configured.
5514459bDeterministic API personas cover anonymous, signed-in-reader, superadmin, development-console, and path-owner authorization.
d4c9dfd5OAuth callback failures retain the legacy 307 redirect contract.
4604fc89Every one of the 218 ported API routes has at least one executable contract case.
063e8fc3All remaining dashboard and admin-console operations require a production superadmin.
d3da1ddcAnonymous parity generation no longer treats legacy personal-data exposure as the favourites contract; authenticated owner and isolation behavior stays in request tests.
5c88dfbdLive comparison checks Go against the response legacy returned in the same run instead of a stale data sample.
c43d7d59Expected-card, shot, and shot-on-target responses preserve the legacy Bet365 bookmaker label independently of whether a team-total line is present.
75f77ac6Team last-game expected statistics retain the legacy numeric-string response contract.
dd8e3990Enriched screener cache hits preserve the cache policy of the original response branch.
f139b1d9/api/player/all rejects an unbounded whole-database aggregate while scoped calls retain exact legacy output; a 686-player live fixture matched field for field.
3a4d42f0Large player-stat aggregates stay below PostgreSQL's 100-argument function limit by building JSON objects in chunks.
fba7ef0eGenerated parity probes reuse curated live-row requests and their required query parameters.
dbfb20e3100 Club keeps public board data and the signed-in watchlist out of its reusable document shell.
5b4c7efaSaved locale hydration no longer races an initial English cookie write in development Strict Mode.
0cf7bb55Public SEO and marketing pages render once instead of duplicating the complete child tree around a locale-cookie boundary.
f1a31cdaSaved filters, both watchlists, tracker settings, and tracked bets reject anonymous requests before body parsing or database access.
181c73a3Value Bets v2/v3 and both web shells share one tested Pro-tier entitlement rule that denies Starter, paused, and expired personas.
5e7da8fcRaw JSON values reach PostgreSQL as JSON text instead of bytea hex, preventing json/jsonb mutation failures.
ec4196aaSaved filters use database-generated identifiers and pass authenticated lifecycle plus cross-account isolation against disposable PostgreSQL.
3a135ef6Tracker settings pass default, upsert, reload, update, normalization, and second-account isolation against disposable PostgreSQL.
5a08a05fTracked bets persist PostgreSQL arrays and generated defaults, then pass add/settle/remove, P&L, and second-account isolation against disposable PostgreSQL.
e96a4fb4Prop watchlist rows use generated identifiers and pass add/reload/duplicate/remove plus second-account isolation against disposable PostgreSQL.
e722be95Value-bet watchlist rows use generated identifiers and pass user-scoped count/duplicate/remove plus second-account isolation against disposable PostgreSQL.
d74a4cc9Team and player favourites pass add/check/notes/list/remove lifecycles with independent second-account state against disposable PostgreSQL.
6588731dBookmaker preferences pass create/reload/update/delete and second-account isolation through the saved-filter contract.
d11013f5Stripe checkout entitlements use generated IDs and pass insert/update/delete propagation without duplicate rows against disposable PostgreSQL.
e0fd8a7eCheckout, plan change, portal, pause, resume, retention, and cancellation handlers pass their Stripe request and PostgreSQL state contracts.
b569b0d2A fail-closed Stripe test-mode probe covers hosted checkout, portal, plan change, pause/resume, retention, cancellation, and cleanup without accepting live keys.
a3515a2eA full five-route, six-level rerun records 104,310 requests, zero non-Home failures, dynamic-route gains, and the remaining public-throughput and cooldown-memory gaps.
408d5bfdLineup alerts use database-generated IDs and pass create/re-arm/list/remove with independent second-account state.
aaa4516cFoul/card alert preferences map muted leagues as a PostgreSQL array and pass update/reload, isolation, disable, and entitlement transitions.
01c7ceffA reusable smoke harness checks critical documents, locales, SEO, basketball host routing, API health/auth denial, and the legacy rollback target against explicit release origins.
10602c89The copied main UI removes all 453 remaining uppercase transforms; lint, typecheck, 940 tests, and desktop/mobile Chromium checks pass.
fd0a9ca5Main-UI canonical and language-alternate tags move from an ineffective client next/head component into App Router server metadata; localized URLs also emit the matching document language.
5dd14027A bounded-output production SEO gate checks sitemap limits, origins, duplicates, canonicals, hreflang, titles, robots, H1 counts, and deterministic page samples.
8872e5f8Production API startup fails closed when auth, billing, bot, internal API, or superadmin credentials are absent.
1626178cProduction billing startup rejects placeholder price IDs and malformed Stripe webhook secrets.
7eec761dA read-only integration audit checks Supabase providers, Stripe prices and webhooks, Telegram delivery state, and Sentry project access.
c00235e3A repeatable cacheability gate rejects private/no-store public documents and can optionally require a fully static response.
cd2ad0beEntity prerender budgets reduce the default warm fan-out from an estimated 15,872 pages to 496 while retaining explicit deployment overrides.
dea250eeReusable classic public routes move out of the session-bearing document tree and retain localized variants.
d03e23a0Signed-in users leave both login aliases and their localized forms through private, no-store redirects.
a2c2b311A session-fed browser/API gate verifies free, Starter, Pro, trialing, paused, expired, and superadmin release personas without creating or printing credentials.

On this page