Mobile app
The StatsHub Expo app — how a screen is put together, where its data comes from, and the rules for writing code in it.
apps/statshub-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 androidWhat it does
Five tabs
Matches, Picks, Screener, Trends and StatsHub. Each is a board; 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 (filters), 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 FlashList 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 pages
Written for someone who just cloned the repo and is about to open their first file. Read them in this order the first time.
Architecture
How a screen is put together.
Data
Where data comes from and how it is cached.
Errors
What happens when something fails.
Conventions
How we write code here.
Resizing
Layout that survives a window changing size.
These describe rules and shapes, not individual files. Anything specific enough to go stale lives in a comment next to the code instead.
The two that are not rules
Features is a list of the pages on statshub.com, the site this app is a port of. App Store covers submission and review.