gRPC & Connect

go.putnami.dev/grpc provides a gRPC server plugin and a Connect protocol gateway for serving gRPC over HTTP/1.1.

Build-time service inventory

The describe pass emits schema/grpc-services.json with the services registered by native gRPC servers, Connect gateways and API bridges. Native registrations include method names and streaming flags, using an unstarted server without binding a listener. Gateways report their mounted prefix and unknown methods; bridges report the exact POST methods they bind. Reflection services appear only when reflection is enabled.

Compose these plugins with Use so describe sees the configured application. One writer emits the complete union, including when describe targets one surface. A configured surface with no services emits an empty list. An absent file leaves the facts unavailable. A successful build removes stale inventory when its producer is removed; a failed describe preserves the committed copy.

See the service inventory contract for the canonical protocol and the route joins for each transport.

gRPC server plugin

Basic setup

import (
    "go.putnami.dev/app"
    "go.putnami.dev/grpc"
)

plugin := grpc.NewPlugin(grpc.Config{
    Port: 9090, // env: GRPC_PORT
})

// Register gRPC services
plugin.Register(func(s *grpc.Server) {
    pb.RegisterUserServiceServer(s, &userServer{})
    pb.RegisterOrderServiceServer(s, &orderServer{})
})

a := app.New("my-service")
a.Module.Use(plugin)
a.ListenAndServe()

Configuration

Field Type Default Env var Description
Port int 9090 GRPC_PORT gRPC server port

Server options

plugin := grpc.NewPlugin(grpc.Config{Port: 9090})

// Add interceptors
plugin.WithUnaryInterceptor(myInterceptor)
plugin.WithStreamInterceptor(myStreamInterceptor)

// Enable server reflection (for grpcurl, etc.)
plugin.WithReflection(true)

// Pass raw grpc.ServerOption
plugin.WithServerOption(grpc.MaxRecvMsgSize(10 << 20))

Built-in interceptors

Logging

Logs each RPC call with method, duration, and status:

import "go.putnami.dev/logger"

plugin.WithUnaryInterceptor(grpc.LoggingInterceptor(logger.Default()))

Recovery

Catches panics in handlers and returns an Internal error:

plugin.WithUnaryInterceptor(grpc.RecoveryInterceptor(logger.Default()))

DI scoping

Creates a DI scope per gRPC request, enabling per-request service resolution:

import "go.putnami.dev/inject"

plugin.WithUnaryInterceptor(grpc.DIInterceptor(containerContext))

Connect protocol gateway

The Connect gateway exposes gRPC services over HTTP/1.1 with JSON encoding, making them accessible from browsers and REST clients.

Setup

import (
    fhttp "go.putnami.dev/http"
    "go.putnami.dev/grpc"
)

server := fhttp.NewServerPlugin(fhttp.ServerConfig{Port: 3000})
gateway := grpc.NewGatewayPlugin(grpc.GatewayConfig{})

// Mount Connect handlers
gateway.MountHandler("/users.v1.UserService/", userConnectHandler)

// Register on the HTTP server
gateway.RegisterOn(server)

a := app.New("my-service")
a.Module.Use(server)
a.Module.Use(gateway)

Calling Connect endpoints

Connect endpoints are accessible as standard HTTP POST requests:

POST /users.v1.UserService/GetUser
Content-Type: application/json

{"id": "user-123"}

Response:

{"name": "Jane", "email": "jane@example.com"}

Lifecycle

Both the gRPC plugin and Connect gateway implement the standard lifecycle interfaces:

  • Configure — register services, configure interceptors
  • Start — begin listening for connections
  • Stop — graceful shutdown with drain
Application.Start()
    → gRPC.Configure()     — register services
    → gRPC.Start()      — listen on :9090
    → Gateway.Configure()  — mount handlers
    → Gateway.Start()   — ready for HTTP traffic

Application.Stop()
    → gRPC.Stop()       — graceful stop

Protobuf contracts

go.putnami.dev/proto renders a .proto service definition from the same api.Plugin route metadata the OpenAPI document is generated from:

apiPlugin := api.New(httpServer)
protoPlugin := proto.NewPlugin(proto.PluginOptions{
    PackageName: "myapp.v1",
    GoPackage:   "example.com/myapp/proto/v1",
}).From(apiPlugin)

a.Use(httpServer).Use(apiPlugin).Use(protoPlugin)

putnami build writes the document to schema/api.proto. The plugin writes nothing while the service is starting or running; to export it outside a Putnami build, call protoPlugin.WriteTo("") after the configure phase.

RPC names are derived from method and path (GET /users → ListUsers, POST /users → CreateUsers), and the api → Connect bridge derives its URLs the same way, so a generated client and the served route agree.

Bridging api endpoints to Connect

grpc.NewApiBridge exposes every bound api.Endpoint at POST /<package>.<Service>/<RPCName> by wrapping the endpoint's own handler, so validation, security, and error shape are identical to a direct REST call.

The request body is a namespaced envelope, which keeps identically named fields from colliding:

{ "params": { "id": "42" }, "query": { "expand": "true" }, "body": { "id": "renamed" } }

The bridge carries JSON only — Protobuf binary encoding and streaming methods are outside its contract — and endpoints finalized with Document() are skipped, since their handlers are mounted elsewhere.

Support and contract

go.putnami.dev/grpc and go.putnami.dev/proto are stable in the workspace support catalog. gRPC behavior is defined by the gRPC services specification and two accepted decision records under go/framework/grpc/; the Protobuf document is one half of the API contracts specification under go/framework/api/specs/, with its own decision record under go/framework/proto/doc/adr/. Before v1.0.0 a minor 0.x release may still contain a documented breaking change; strict compatibility between every pre-1.0 minor is not promised.