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
| Level | Meaning |
|---|---|
| Runtime verified | Exercised against a running replacement stack with the expected result. |
| Source verified | The legacy behavior and replacement implementation were traced, but the complete user flow has not run. |
| Partial | The implementation exists, but coverage or behavior remains unverified. |
| Fail | A 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
| Surface | Current evidence | Status |
|---|---|---|
| Customer page routes | All 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 implementations | All 73 customer page source files have a matching file under src/composed/main-ui/pages. | Source verified |
| Shared components | All 312 legacy component paths have a matching file under src/components/main-ui. | Source verified |
| React contexts | All 8 legacy context paths have a matching file under src/lib-client/legacy/contexts. | Source verified |
| Legacy API routes | The generated Go port ledger records 218 of 218 routes as ported. | Source verified |
| API contract cases | The 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 case | All 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
| Capability | Legacy contract | Replacement evidence | Status |
|---|---|---|---|
| Guest browsing | Most 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-in | Login 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 policy | Supabase 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 features | Account, 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 tools | Value 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 controls | Stored 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 management | Pricing 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 alerts | Paid 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 access | Bot 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 Portuguese | Locale 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 subdomain | basketball.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 content | Entity 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.
| Integration | Evidence | Status |
|---|---|---|
| Supabase Auth | /auth/v1/settings returned HTTP 200 with email and Google enabled. | Pass |
| Stripe | Both 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 |
| Telegram | The 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 |
| Sentry | The 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=testAuthenticated 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.jsonEach 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/sessionsFor 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
| Gate | Required result | Current state |
|---|---|---|
| Auth personas | Guest, 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 mutations | Favourites, 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 lifecycle | Checkout, 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 lifecycle | Link, 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 routing | Root and nested basketball subdomain URLs match legacy, including excluded auth and API paths. | Pass |
| API contract depth | Every 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 suite | The full web test command passes. | Pass: 1,029 tests across 63 files |
| Web lint | ESLint evaluates the source and the complete lint script passes. | Pass: ESLint completes with 0 errors and check:caps reports no uppercase text transforms |
| Production build | Next.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 crawl | Sitemaps 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 capacity | The 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 monitoring | Sentry 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 rollback | Critical 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
| Commit | Result |
|---|---|
d220cb6e | OAuth callbacks honor the forwarded replacement host in development while retaining the direct legacy fallback. |
4387364b | Canonical routes run the copied main-UI session and protected-route policy. |
06d6d851 | ES/PT selection survives redirect, internal rewrite, session-cookie refresh, and client hydration. |
d1486419 | Basketball subdomain requests rewrite into the basketball route tree while preserving legacy exclusions. |
a6d9e2a9 | Production header tests protect personalized APIs without disabling the public route cache. |
b130f23a | The experimental bet builder uses the title-case Corners translation expected by its market model. |
1756a720 | One-character player-name typos reach fuzzy matching instead of being mislabeled as abbreviations. |
65dce665 | Assistant comparisons explicitly parallelize independent lookups within one topic. |
613656ac | Web lint resolves a supported TypeScript compiler instead of crashing inside ts-api-utils. |
4342fdc8 | ESLint completes its full source scan with no blocking errors. |
bb28ebd3 | Paused subscriptions are denied consistently by both web shells, server-side web guards, and Go premium endpoints. |
cc95644b | Super Sub Heroes rejects anonymous Go API requests before cache or database access, matching the legacy premium gate. |
94ce4bcb | Favourite reads, writes, note edits, and checks require the authenticated owner instead of trusting a caller-supplied user id. |
10ba304e | Bot routing changes and Telegram broadcasts require a production superadmin. |
519690dd | Nineteen Telegram-dashboard reads and controls require a production superadmin. |
670fc44d | Operational logs, stale-odds data, daily reports, and bulk league exports require a production superadmin. |
7c6c4320 | Both MCP transports enforce MCP_API_KEY for JSON-RPC requests when it is configured. |
8feafa15 | Telegram webhook updates require the registered secret-token header and fail closed when production is not configured. |
5514459b | Deterministic API personas cover anonymous, signed-in-reader, superadmin, development-console, and path-owner authorization. |
d4c9dfd5 | OAuth callback failures retain the legacy 307 redirect contract. |
4604fc89 | Every one of the 218 ported API routes has at least one executable contract case. |
063e8fc3 | All remaining dashboard and admin-console operations require a production superadmin. |
d3da1ddc | Anonymous parity generation no longer treats legacy personal-data exposure as the favourites contract; authenticated owner and isolation behavior stays in request tests. |
5c88dfbd | Live comparison checks Go against the response legacy returned in the same run instead of a stale data sample. |
c43d7d59 | Expected-card, shot, and shot-on-target responses preserve the legacy Bet365 bookmaker label independently of whether a team-total line is present. |
75f77ac6 | Team last-game expected statistics retain the legacy numeric-string response contract. |
dd8e3990 | Enriched 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. |
3a4d42f0 | Large player-stat aggregates stay below PostgreSQL's 100-argument function limit by building JSON objects in chunks. |
fba7ef0e | Generated parity probes reuse curated live-row requests and their required query parameters. |
dbfb20e3 | 100 Club keeps public board data and the signed-in watchlist out of its reusable document shell. |
5b4c7efa | Saved locale hydration no longer races an initial English cookie write in development Strict Mode. |
0cf7bb55 | Public SEO and marketing pages render once instead of duplicating the complete child tree around a locale-cookie boundary. |
f1a31cda | Saved filters, both watchlists, tracker settings, and tracked bets reject anonymous requests before body parsing or database access. |
181c73a3 | Value Bets v2/v3 and both web shells share one tested Pro-tier entitlement rule that denies Starter, paused, and expired personas. |
5e7da8fc | Raw JSON values reach PostgreSQL as JSON text instead of bytea hex, preventing json/jsonb mutation failures. |
ec4196aa | Saved filters use database-generated identifiers and pass authenticated lifecycle plus cross-account isolation against disposable PostgreSQL. |
3a135ef6 | Tracker settings pass default, upsert, reload, update, normalization, and second-account isolation against disposable PostgreSQL. |
5a08a05f | Tracked bets persist PostgreSQL arrays and generated defaults, then pass add/settle/remove, P&L, and second-account isolation against disposable PostgreSQL. |
e96a4fb4 | Prop watchlist rows use generated identifiers and pass add/reload/duplicate/remove plus second-account isolation against disposable PostgreSQL. |
e722be95 | Value-bet watchlist rows use generated identifiers and pass user-scoped count/duplicate/remove plus second-account isolation against disposable PostgreSQL. |
d74a4cc9 | Team and player favourites pass add/check/notes/list/remove lifecycles with independent second-account state against disposable PostgreSQL. |
6588731d | Bookmaker preferences pass create/reload/update/delete and second-account isolation through the saved-filter contract. |
d11013f5 | Stripe checkout entitlements use generated IDs and pass insert/update/delete propagation without duplicate rows against disposable PostgreSQL. |
e0fd8a7e | Checkout, plan change, portal, pause, resume, retention, and cancellation handlers pass their Stripe request and PostgreSQL state contracts. |
b569b0d2 | A fail-closed Stripe test-mode probe covers hosted checkout, portal, plan change, pause/resume, retention, cancellation, and cleanup without accepting live keys. |
a3515a2e | A 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. |
408d5bfd | Lineup alerts use database-generated IDs and pass create/re-arm/list/remove with independent second-account state. |
aaa4516c | Foul/card alert preferences map muted leagues as a PostgreSQL array and pass update/reload, isolation, disable, and entitlement transitions. |
01c7ceff | A reusable smoke harness checks critical documents, locales, SEO, basketball host routing, API health/auth denial, and the legacy rollback target against explicit release origins. |
10602c89 | The copied main UI removes all 453 remaining uppercase transforms; lint, typecheck, 940 tests, and desktop/mobile Chromium checks pass. |
fd0a9ca5 | Main-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. |
5dd14027 | A bounded-output production SEO gate checks sitemap limits, origins, duplicates, canonicals, hreflang, titles, robots, H1 counts, and deterministic page samples. |
8872e5f8 | Production API startup fails closed when auth, billing, bot, internal API, or superadmin credentials are absent. |
1626178c | Production billing startup rejects placeholder price IDs and malformed Stripe webhook secrets. |
7eec761d | A read-only integration audit checks Supabase providers, Stripe prices and webhooks, Telegram delivery state, and Sentry project access. |
c00235e3 | A repeatable cacheability gate rejects private/no-store public documents and can optionally require a fully static response. |
cd2ad0be | Entity prerender budgets reduce the default warm fan-out from an estimated 15,872 pages to 496 while retaining explicit deployment overrides. |
dea250ee | Reusable classic public routes move out of the session-bearing document tree and retain localized variants. |
d03e23a0 | Signed-in users leave both login aliases and their localized forms through private, no-store redirects. |
a2c2b311 | A session-fed browser/API gate verifies free, Starter, Pro, trialing, paused, expired, and superadmin release personas without creating or printing credentials. |
Related
- Legacy web access gates — guest, free, paid, top-tier, and superadmin behavior in legacy.
- API parity stack — run the live legacy-versus-Go contract comparison.
- Website rebuild performance — current throughput, latency, payload, and memory results.
- Web application overview — replacement web architecture and commands.