Otto talks to three clients — web (React), mobile (React Native), and desktop (Tauri). Three platforms, one server, and a single typed contract that keeps everyone honest.
The premise
Type drift between clients and server is the most boring bug class on Earth, which is exactly why it eats hours. We wanted a setup where adding a field on the server made TypeScript scream on every client until it was handled — no runtime surprises, no defensive as any parsing.
Picking the source of truth
The server is the source of truth. We use Pydantic on FastAPI and emit the schema as OpenAPI. Both clients consume the schema; nothing on a client gets to define a new shape.
class TaskV1(BaseModel):
id: UUID
title: str
completed: bool
due: datetime | NoneShape, then schema
We codified one rule: model the shape of the data before you write the database schema. Pydantic first, SQLAlchemy second. This forces UX-relevant shapes (composite fields, computed flags) to be first-class, not a JOIN waiting to happen.
Generating clients
openapi-typescript turns the OpenAPI doc into a .d.ts file. Both the web and desktop apps import directly from it. React Native goes through the same path with a tiny shim for native modules. No hand-written types, no copy-paste.
Migrations across platforms
When the server adds a field, the contract bumps. Every client gets a PR titled "contract bump" that has only one change: the regenerated type file. The CI build then fails everywhere a downstream needs to handle the new field. It's the cheapest cross-team coordination we've ever shipped.
What it costs to keep
About 30 minutes of CI a week, plus the discipline to never add a manual type. The wins: shipping the same feature to three clients in a single PR, no live-incident debugging of mismatched payloads, and onboarding new devs in hours instead of days.
Six lessons, taped to the fridge
- One source of truth. Pick the server. Defend it.
- Generate, never hand-write. Hand-written types drift on contact with humans.
- Compose, don't inline. Shared shapes belong in a shared file.
- Break the build, not production. Type errors are cheaper than runtime errors.
- Ship the contract first. Every feature starts with a typed shape.
- Audit the boundary. Where types meet network, log liberally for a week.