Packages
Shared code under packages/ — what each one owns and which apps consume it.
A package is code more than one app imports. One consumer is not a package; it
is a directory in that app. Every package is named for its directory — statshub-<name> — flat under
packages/.
ui-web
Design-system primitives on Base UI. Consumed by web and admin.
ui-native
The React Native component kit. Consumed by expo.
api-client
JSON transport, auth-header seam and typed HTTP errors. Consumed by web and expo.
api-contract
Records and replays API responses to prove the Go port faithful.
Two UI kits, and no third
There is no shared component layer and there cannot be one: expo is React
Native, web and admin target the DOM, and there is no react-native-web in
this repository. A component cannot cross that line, so the kits are parallel
rather than shared.
HTTP behavior is shared; platform policy is not
statshub-api-client exists because web and Expo
both need the same URL serialization, JSON parsing, auth-header injection,
cancellation handling, and typed error. Removing it would put those rules back
into both apps, which is the package test.
Next.js revalidation and cookie handling stay in web. React Query, Cloudflare-specific retry and SecureStore stay in Expo. Those are adapters or platform policy, not transport behavior.
One identity, two shapes
Both apps now sign in against the same Supabase project, and the Go API
validates its tokens either way — as a bearer header or as the @supabase/ssr
cookie. getHeaders is still only injection: the receiving server validates
what it is handed.
Conventions
Publish TypeScript source, not build output. Every consumer bundles it, so a build step is only a cache to invalidate.
"dependencies": { "@statshub/ui-web": "workspace:*" }Turbo infers build order from the dependency, so ^build already covers it.
Add a package has the full walkthrough, including
the Metro rules that make native packages different.
Go against the Next.js routes
Both stacks on one box and one database, loaded from 1 to 2,048 concurrent connections until they stopped answering. Go serves 39× the requests on work that touches no database, and exactly the same number on work that does.
statshub-config
What every board shows, what it starts as, and who may see it — as JSON both apps read.