Three boundaries, one source of truth: sharing types across Rust and TypeScript
Three boundaries, one source of truth: sharing types across Rust and TypeScript
When you have a Rust backend and a TypeScript frontend, the dream is one schema, typed everywhere. I spent a day this week convincing myself the answer was "protobuf all the way down." It wasn't — and the reason turned out to be the whole lesson.
The app has three boundaries that all need to agree on data shapes:
- Solana validator → indexer — a firehose of account and transaction updates.
- Indexer → web app — the public read API the frontend calls.
- On-chain program → web app — account and instruction types coming off Solana RPC.
The problem is the three boundaries don't want the same thing.
Ingestion (validator → indexer) genuinely is a gRPC job. It's a high-throughput, server-to-server stream, and the Solana indexing ecosystem has largely converged on Yellowstone/Geyser gRPC (over tonic + prost). Protobuf's compact wire format and backpressure are exactly right here. Keep it. The public API (indexer → web) is where "protobuf everywhere" falls apart. This is a browser client hitting cacheable read endpoints through an edge/CDN. gRPC-Web there makes standard browser/CDN caching less natural, usually needs proxy or server support, and gives me worse debuggability — to save a few bytes on payloads the CDN was going to cache anyway. So this boundary stays REST. The type safety comes from codegen instead: Rust response structs, via JSON Schema/OpenAPI generation (aide + schemars), produce an OpenAPI 3.0 spec →openapi-typescript → the exact types the frontend imports. A CI check fails the build if the generated types ever drift from the Rust source. That's the same kind of no-drift guarantee tRPC gives you — single source of truth — just across a language boundary tRPC can't cross. Bonus: the OpenAPI spec gives me free interactive docs.
On-chain types have only one honest source: the Anchor IDL. It isn't a transport at all — it's the program's interface description — and Anchor generates TypeScript types straight from it. Nothing to invent.
So here's what I actually learned: "share the API" is not a transport decision, it's a codegen-discipline decision. The win isn't one wire format everywhere — it's one source of truth per boundary, every TS type generated (never hand-written), and a drift gate in CI. Three boundaries, three generators, one rule.
If I ever do want protobuf reused literally in the browser, the clean path is ConnectRPC rather than raw gRPC-Web. It can support cacheable GETs for side-effect-free reads — so it doesn't necessarily give up edge caching — but I'd still be choosing an RPC/protobuf surface over the simpler REST/OpenAPI surface I want for a public market API.
Reaching for one tool to unify everything felt like good engineering. Matching the tool to each boundary turned out to be better engineering.

Oleksandr Yusypenko
Senior Full-Stack + AI Engineer. Building in public — AI agents, LangGraph, production systems.
Related Posts
Jun 14, 2026
Streaming Live Solana Trades Through Cloudflare Workers (The Wire-Splice Trick)
How I move on-chain data end to end — ingesting Solana into a database from a Rust server, then streaming it live to a Next.js app through a Cloudflare Worker that acts as a zero-copy SSE wire splice.
Apr 8, 2026
Building an AI Content Engine for a Gov Contracting Platform
Government contracting is jargon-heavy and the content gap is huge. Here's the four-layer AI content engine I built for GovChime: a research agent, rubric scoring, an approval queue, and SEO-ready Next.js publishing.
Mar 9, 2026
Agent Builders Are Changing How I Ship Code — Here's My Actual Workflow
I've been shipping production features as a solo engineer on a complex multi-service codebase. The secret isn't working harder — it's building custom AI agents that know my architecture. Here's the exact setup I use with Claude Code.
How I Redesigned a Meal Planning App UX: From Panel to Modern Food-First Design
NextHiding an Internal API the Right Way: Why a Proxy Isn't Security