Typed boundaries
Turn unknown into a typed value — res.json(), a Supabase select/RPC data, any payload you didn’t build here — with a Zod .parse(), never an as cast or a hand-rolled interface. A cast asserts a shape nobody checked; .parse() checks it, and both ends share one schema, so a mismatch is a parse error at the boundary instead of a TypeError later. It’s the Types rule at the JSON boundary; arbe’s wire schemas live in @arbe/core/schemas/.
const body = (await res.json()) as { id: string } // ✗ uncheckedconst { id } = CreateThreadResponseSchema.parse(await res.json()) // ✓ checked, sharedWhere schemas go:
- Client envelopes (
CreateThreadResponse,VersionInfo) →client-responses.ts. - Domain shapes → beside their domain (
workflow.ts,usage.ts,secret.ts,sandbox.ts); a narrowselectreusesRowSchema.pick({ … }). - www request bodies →
readJsonBody(event, schema)(validate.ts), which 400s on bad input.
Move a local schema into core once a second surface needs it — don’t copy it.
A cast is still fine where the code can’t own a core schema:
- Core-free packages like
workflow-conductor— the www route that produces the data parses it, the sibling trusts that. - Generic transport (
readStream<T>(),requestJson<T>()) — the caller names the schema. - Third-party payloads (GitHub
latest.json, Tenor, sprites) — parsed where they enter, by whoever owns them. - Error envelopes you only stringify.
So parse a core schema when your code depends on @arbe/core and reads one of arbe’s own shapes — in practice www and core. Convert casts as you find them; a list in a doc just goes stale.