MVP Factory
ai startup development

gRPC-Web on mobile without a proxy: Connect Protocol

KW
Krystian Wiewiór · · 5 min read

Meta description: Drop Envoy, keep type safety. Connect Protocol brings native HTTP/2 streaming to Android and iOS — no proxy, full protobuf codegen, composable interceptors.

Tags: kotlin, android, ios, grpc, api, mobile


TL;DR

Standard gRPC-Web requires an Envoy sidecar to translate between browser/mobile HTTP and gRPC’s HTTP/2 framing. Connect Protocol from Buf eliminates that proxy entirely — Connect-Kotlin and Connect-Swift speak HTTP/2 natively, remain wire-compatible with gRPC, and ship a composable interceptor pipeline for auth, retry, and observability. The protocol comparison favors it. The developer experience is cleaner in ways that actually matter.


The proxy tax you’re paying right now

What most teams get wrong about mobile gRPC: they treat the Envoy proxy as a given. It is not.

The canonical gRPC-Web architecture looks like this:

Mobile client → Envoy (translate framing) → gRPC backend

That Envoy hop adds operational surface area — another service to deploy, version, and monitor. On mobile, where you control the HTTP stack directly, the translation layer is unnecessary. gRPC-Web exists because browsers cannot speak raw HTTP/2 framing. Mobile apps can. Yet most teams cargo-cult the proxy anyway.

Connect Protocol, maintained by Buf, fixes this properly. It defines a lightweight envelope over HTTP/1.1 and HTTP/2 that is both browser-friendly and natively gRPC-compatible — no sidecar required.


Protobuf code generation: Gradle and Xcode

Note: Pin dependency versions against the official Connect-Kotlin and Connect-Swift release pages before shipping — versions in the examples below will drift.

Android (Gradle)

// build.gradle.kts
plugins {
    id("com.google.protobuf") version "0.9.4"
}

dependencies {
    implementation("com.connectrpc:connect-kotlin-okhttp:0.6.0")
    implementation("com.connectrpc:connect-kotlin:0.6.0")
}

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:3.25.1" }
    plugins {
        create("connect-kotlin") {
            artifact = "com.connectrpc:protoc-gen-connect-kotlin:0.6.0:jvm8@jar"
        }
    }
    generateProtoTasks {
        all().forEach { task ->
            task.plugins { create("connect-kotlin") }
        }
    }
}

This generates both the protobuf models and the Connect-flavored service stubs in a single Gradle task. No separate script step.

iOS (Xcode + Swift Package Manager)

// Package.swift
dependencies: [
    .package(url: "https://github.com/connectrpc/connect-swift", from: "0.12.0"),
],
targets: [
    .target(
        name: "MyApp",
        dependencies: ["Connect"]
    )
]

Proto generation on iOS uses buf generate with the connect-swift plugin. Wire this into an Xcode build phase or a Makefile pre-build step — it outputs typed ProtocolClient stubs directly.


Connect vs REST: feature and protocol comparison

Serialization format is the highest-leverage variable here. Google’s protobuf benchmarks show serialization running 3–10× faster than JSON with payloads 20–60% smaller for equivalent message structures. For a typical API response — say, a list of 50 user records — that works out to roughly 1.2 KB protobuf vs. 3.8 KB JSON, a difference that compounds under mobile data constraints. How the approaches compare across dimensions that move the needle in production:

DimensionOkHttp REST (JSON)URLSession REST (JSON)Connect-KotlinConnect-Swift
Payload sizeVerbose JSONVerbose JSONBinary protobuf (~3–5× smaller)Binary protobuf (~3–5× smaller)
Connection reuseKeep-alive (HTTP/1.1)Keep-alive (HTTP/1.1)HTTP/2 multiplexingHTTP/2 multiplexing
StreamingPolling or SSE workaroundsPolling or SSE workaroundsNative bidirectionalNative bidirectional
Type safetyRuntime (Gson/Moshi)Runtime (Codable)Compile-time (proto)Compile-time (proto)
Code generationManual or OpenAPIManual or OpenAPIbuf generate → stubsbuf generate → stubs
Proxy requiredNoNoNoNo

HTTP/2 eliminates HTTP-level head-of-line blocking, meaningfully reducing latency variance on high-latency mobile connections. TCP-level head-of-line blocking remains — that requires HTTP/3 over QUIC to fully resolve — but removing the HTTP layer alone is a real win on lossy LTE and satellite connections.


Bidirectional streaming over HTTP/2

// Connect-Kotlin: bidirectional stream
val stream = client.eliza().converse()

launch {
    stream.responseChannel().collect { response ->
        println(response.sentence)
    }
}

stream.send(ConverseRequest { sentence = "Hello" })
stream.close()

This is real bidirectional streaming — not long polling, not chunked SSE. Both the send and receive channels are live simultaneously over a single HTTP/2 stream. URLSession on iOS exposes the same primitive via Connect-Swift’s BidirectionalStreamInterface.


The interceptor pipeline

This is where Connect pulls significantly ahead for production use. The interceptor chain composes cleanly:

val client = ProtocolClient(
    httpClient = ConnectOkHttpClient(OkHttpClient()),
    config = ProtocolClientConfig(
        host = "https://api.example.com",
        serializationStrategy = GoogleJavaProtobufStrategy(),
        interceptors = listOf(
            ::AuthInterceptor,
            ::RetryInterceptor,
            ::TracingInterceptor
        )
    )
)

Auth headers, exponential backoff retry, and OpenTelemetry span injection all live in the interceptor chain — not scattered across individual call sites. No Envoy filter chains to configure. No sidecar YAML to maintain.


Conclusion

Connect Protocol is not a workaround — it is the correct architecture for mobile gRPC today. The proxy was always a concession to browser limitations. Mobile clients never had that limitation.

Three things worth doing before you ship:

Audit your proxy topology first. If Envoy exists solely to serve mobile clients rather than browsers, it is a candidate for removal. Connect’s HTTP/2 native transport is a drop-in replacement at the wire level.

Treat buf generate output as a CI artifact, not checked-in source. Both Android and iOS pipelines can generate typed clients from the same .proto source of truth — there is no reason to commit generated files.

Centralize cross-cutting concerns in the interceptor chain. Auth, retry, and observability belong in the protocol layer, not scattered across individual repository or use-case classes. Connect’s interceptor API makes this the path of least resistance on both platforms.


Share: Twitter LinkedIn