Level 9 /

Protobuf & gRPC

Communication between stations needs a common language: structured messages that both sides understand. Protocol buffers are the message format, gRPC is the wire.

Gopher says

Protocol Buffers are a language-neutral binary serialization format, and gRPC is the RPC framework built on top of them. Together they give you type-safe schemas, code generation, and machine-enforced API contracts.

In baton-junction, every v2.Resource, v2.Grant, and v2.Entitlement you’ve seen comes from protobuf schemas compiled through gRPC. The SDK generates the Go types - we just use them.

The Type Pipeline

Every v2.* type in a Baton connector starts as a .proto schema definition, gets compiled into Go structs, and is published in the SDK. You never write .proto files - you use the generated types.

The import path tells the story:

import v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"

The pb means “protocol buffers” - everything under that path is generated code.

Why gRPC, Not REST?

REST APIs use JSON over HTTP - flexible, human-readable, and everywhere. But flexibility has a cost: nothing enforces that a server’s response matches its documentation. Field names, types, and structures can drift silently.

gRPC flips this. It uses protobuf as its serialization format and HTTP/2 as its transport. Where REST says “send JSON to this URL,” gRPC says “call this function with these typed arguments.” The .proto file is the API contract - server and client code are both generated from it. If one side changes the schema, the other won’t compile until it’s updated.

REST/JSONgRPC/Protobuf
FormatText (human-readable)Binary (compact)
SchemaOptional (OpenAPI, Swagger)Required (.proto files)
EnforcementRuntime (if at all)Compile time
Code generationOptionalBuilt in
TransportHTTP/1.1 or HTTP/2HTTP/2 (multiplexed)

When a platform like C1 connects thousands of connectors built by different developers, this rigidity is a feature. Miscommunication between connector and platform doesn’t show up as subtle runtime bugs - it shows up as compilation failures you fix before shipping.

Two Protocols Coexist

A connector sits between two different communication protocols:

REST for the target app. The application you’re connecting to already has a REST API - you can’t change it. The client/ package uses standard HTTP to talk to this API.

gRPC for the SDK. The connector framework uses gRPC internally for type safety, code generation, and streaming. Your connector talks to the target app’s REST API via the SDK’s HTTP client (uhttp.BaseHttpClient), and the SDK uses gRPC for connector-to-platform communication. That’s why you see both HTTP client calls and v2.Resource types in the same codebase.

Gopher says

gRPC might seem like overkill for a connector, but it solves a real problem: when thousands of connectors talk to the same platform, you need machine-enforced contracts, not documentation that might be out of date.

In baton-junction, we use REST to talk to the target API (the client/ package) and gRPC types to talk to the C1 platform (every v2.* type). Two protocols, each used where it fits.

Resource Types

Before we look at the code, know that resource types are the vocabulary your connector uses - they declare what the connector syncs (users, groups, roles, and so on). Here’s the code from baton-junction:

keywordpointer to struct literalprotobuf generated typeprotobuf enum (SCREAMING_SNAKE)
var userResourceType = &v2.ResourceType{
    Id:          "user",
    DisplayName: "User",
    Traits:      []v2.ResourceType_Trait{v2.ResourceType_TRAIT_USER},
}

Traits tell the platform what capabilities the resource has: TRAIT_USER means “this is a person,” TRAIT_GROUP means “this can have members.”

The Builder Pattern

Protobuf-generated structs have many fields, optional pointers, and internal nesting - constructing them by hand is error-prone and tedious. That’s why you don’t construct v2.Resource structs directly: builders handle the complexity. Here’s how baton-junction builds a user resource:

r, err := resource.NewUserResource(
    displayName(u.FirstName, u.LastName, u.Email, u.ID),
    userResourceType,
    u.ID,
    opts,
    resource.WithAnnotation(&v2.RawId{Id: u.ID}),
)

Builders validate inputs, set defaults, and hide internal protobuf structure. The With* functions are functional options - Go’s answer to keyword arguments.

Functional Options in Action

The beauty of this pattern is flexibility: you can mix and match options depending on what each resource needs, without changing function signatures. Options are assembled conditionally - include only what’s relevant:

opts := []resource.UserTraitOption{
    resource.WithEmail(u.Email, true),
    resource.WithUserLogin(u.Username),
    resource.WithStatus(mapUserStatus(u.Status)),
}

if u.LastLogin != nil {
    opts = append(opts, resource.WithLastLogin(*u.LastLogin))
}

The Nil-Check-Then-Dereference Pattern

That if u.LastLogin != nil guard followed by *u.LastLogin is a pattern worth calling out. Protobuf-generated types use pointers heavily to distinguish “not set” from “zero value.” For example, *time.Time means the timestamp is optional, while time.Time always has a value (even if it’s the zero time). You must always check for nil before using * to dereference, or your program will panic at runtime. You’ll see this guard-then-dereference pattern frequently in protobuf-heavy code, especially when extracting optional fields from SDK types.

New options can be added to the SDK without breaking existing code. You only specify what you need.

Fill in the three fields on the repoResourceType variable: Id, DisplayName, and Traits. The resource represents repositories, which can have members.

Complete the groupResource function by calling the SDK builder to create a group resource. Pass the group’s display name, resource type, ID, a group profile with the description, and an annotation preserving the original ID.

Gopher says

Protobuf gives you type safety at compile time. gRPC gives you machine-enforced API contracts. Code generation turns .proto schemas into Go structs you can use directly. And the builder pattern with functional options makes constructing those types safe and flexible.

In baton-junction, every v2.* type comes from protobuf code generation, and we use SDK builders like resource.NewUserResource with With* options to construct them.

Next up: testing!

Next Lesson

Testing

Continue