My stack The stack · Expo

Expo and React Native

Two mobile apps, and neither of them owns a colour.

2Expo apps, each beside the API it talks to
1token package per product, read by every platform
0.86React Native, pinned at the workspace root with React 19
01Why the tokens live outside the app

The failure mode with a product on web and mobile is not that they look different on day one. It is that they drift — a colour is nudged on the web, the phone keeps the old one, and six months later nobody can say which is correct because both are written down in two places.

So the tokens are a workspace package. @tether/design for one, packages/tokens for Coursified, which serves web and mobile at once. A colour or a type step is declared once and imported everywhere, and there is no second place to change it.

02One version of React, enforced at the root Two copies of React in a native tree is a class of bug you cannot debug from the symptom.
The pin

Overrides at the workspace root, not hopeful ranges in each app.

root package.jsonoverrides
react 19.2.3exactly
react-native 0.86.3exactly
safe-area-context, svgpinned too

Tether’s root package.json pins React, React DOM, React Native, react-native-safe-area-context and react-native-svg by exact version in overrides. In a workspace, two packages asking for compatible-but-different versions is normal resolution behaviour — and in a native tree it produces hooks that fail across a boundary for no visible reason. The pin is not caution; it is the difference between a build that works and a day lost to a symptom with no stack trace.

03What porting a design to native actually costs Measurement, not eyeballing.
Fonts fall back silently

A face that is not registered does not error — it renders in the system font and looks almost right. The only way to know is to check, per platform.

Shadows are not portable

iOS shadow properties do nothing on Android, which wants elevation. A card that reads correctly on one looks flat on the other.

Safe-area insets move everything

The inset differs by device and by orientation, so a layout verified on one simulator is not verified.

The token package is the fix

None of the above is solved by being careful. It is solved by having one declaration site and checking the rendered result against it.

A design that survives the trip into React Native survives it because somebody measured, not because somebody was careful.

the working rule behind both apps
04The repository
The case study

Tether — apps/mobile and packages/design

The mobile app and the token package sit in the same Bun workspace as the API, so a change to a shared type breaks all three builds at once rather than one of them at runtime.

The phone and the web look like the same product because there is one place to change.

That is the whole trick, and it is organisational rather than technical: the token package removes the option of two answers.