StatsHub Docs

Overview

The StatsHub app for iOS and Android — what it does, how a screen is put together, and where its data comes from.

apps/expo is the StatsHub app for iOS and Android — the boards from the website as a native app, plus a few the site does not have.

bun run dev:expo       # Metro on 8142
bun run ios            # or: bun run android
Expo SDK 57Router, expo-widgetsReact Native 0.86Reanimated 4workletsTypeScriptuniwindTailwind for nativeTanStack Queryall server stateZustandpreferences and durable client stateConvexprofiles and accountsBetter AuthRevenueCatsubscriptionsPostHogiOSAndroid

What it does

Four tabs

StatsHub, Matches, Trends and Props. Each board has a detail stack behind it for a fixture, team, player, league, referee or prop.

Filters as sheets

Leagues, players, bookmakers, stats, ranges, games, availability, sort — a route each under filter-sheets or a feature-owned filter stack, so a filter panel is navigable, deep linkable, and dismissible the way the platform expects.

Saved views and defaults

A screener setup is saved, named and reopened. board-card-settings/[board] goes further and lets you choose what each card shows.

Prop Hunter, step by step

The website's board rebuilt as a wizard — prop-hunter/step/[step] — because twelve filters side by side is a desktop shape, not a phone one.

Widgets and reminders

Home-screen widgets for a countdown, a live score and a hit rate, each configurable from the widget itself. Reminders for a fixture you are watching.

Boards the website does not have

Anytime Goalscorer, Assists, Cards, Super Subs and a Leagues browser. All five 404 on statshub.com — they are the app's own.

How it is built

One declaration per endpoint

defineQuery in src/lib/api/resource.ts describes an endpoint once — key, fetch, staleness — and hands back the hook, the imperative fetch, the prefetch and the invalidation, all agreeing on that key by construction. It exists to rule out the usual bug: a mutation invalidating ["referees"] while the screen reads ["referee-list"].

Every sheet is a route

Expo Router all the way down, including the filter panels. A modal that is not a route cannot be linked to, cannot be restored, and does not survive the back gesture.

Animation runs off the JS thread

Reanimated 4 worklets and Gesture Handler, with a recycling Legend List under every long board. A scroll-linked header that reads a shared value on the JS thread is the single most common way a screen here starts dropping frames.

Layout survives a resizing window

iPad multitasking, Stage Manager and a rotating phone all change the size of a mounted screen. The rules for that are their own page because getting them wrong is invisible on a simulator at one size.

Two backends, split by what they hold

Match data comes from api. Accounts do not: Convex holds the profile table and Better Auth the session, so signing in does not touch the stats service at all. RevenueCat decides what is unlocked, and /paywall is a route like any other.

Failure has a defined shape

What a screen shows when a query fails, when it is empty, and when it is still loading is decided once and applied everywhere, rather than per screen by whoever wrote it.

The rest of this section

Read them in this order the first time. They describe rules and shapes, not individual files — anything specific enough to go stale lives in a comment next to the code instead.

On this page