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/JSON | gRPC/Protobuf | |
|---|---|---|
| Format | Text (human-readable) | Binary (compact) |
| Schema | Optional (OpenAPI, Swagger) | Required (.proto files) |
| Enforcement | Runtime (if at all) | Compile time |
| Code generation | Optional | Built in |
| Transport | HTTP/1.1 or HTTP/2 | HTTP/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:
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.
Generated protobuf code uses underscore naming (ResourceType_TRAIT_USER,
_builder structs, Get*() getters). Hand-written SDK code uses idiomatic Go
(NewUserResource, WithEmail). The getter methods like GetLogin() are
nil-safe - if the parent is nil, they return zero values instead of panicking.
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

