Custom gRPC Adapters
This guide shows you how to create custom gRPC adapters for Passage, enabling you to implement complex routing logic, integrate with existing systems, and extend Passage’s functionality using any programming language.
Why gRPC Adapters?
Section titled “Why gRPC Adapters?”gRPC adapters allow you to:
- Implement custom business logic in any language with gRPC support
- Integrate with existing infrastructure (databases, APIs, services)
- Maintain separation of concerns between Passage and your application logic
- Scale independently from Passage
Prerequisites
Section titled “Prerequisites”- Basic understanding of gRPC and Protocol Buffers
- A programming language with gRPC support (Go, Java, Python, Node.js, Rust, etc.)
- The Passage proto definitions
Proto Definitions
Section titled “Proto Definitions”Passage provides proto definitions for all six gRPC services. These are available in the repository:
Directorypassage-adapters/grpc/proto/adapter/
- adapter.proto Common types (Target, Address, MetaEntry, Profile, ClientInfo, PlayerInfo)
- status.proto Status service
- authentication.proto Authentication service
- discovery.proto Discovery service
- discovery_action.proto Discovery Action service
- localization.proto Localization service
Available gRPC Services
Section titled “Available gRPC Services”Passage uses gRPC in four adapter categories. Each service handles a different phase of the connection:
| Service | Proto File | Config type | Purpose |
|---|---|---|---|
Status | status.proto | grpc (in status) | Server list ping response |
Authentication | authentication.proto | grpc (in authentication) | Player identity verification |
Discovery | discovery.proto | grpc_discovery (in discovery) | Backend server discovery |
DiscoveryAction | discovery_action.proto | grpc (in discovery.actions) | Target list transformation |
Localization | localization.proto | grpc (in localization) | Disconnect message translation |
See the gRPC Protocol Reference for complete message definitions.
Example: Status Adapter in Go
Section titled “Example: Status Adapter in Go”-
Set up your project and generate code from the proto files
Terminal window mkdir passage-status-adapter && cd passage-status-adaptergo mod init github.com/yourorg/passage-status-adapter# Copy protos from the Passage repositorymkdir -p proto/adaptercp /path/to/passage/passage-adapters/grpc/proto/adapter/*.proto proto/adapter/# Generate Go codeprotoc --go_out=. --go_opt=paths=source_relative \--go-grpc_out=. --go-grpc_opt=paths=source_relative \proto/adapter/*.proto -
Implement the Status service
package mainimport ("context""log""net"pb "github.com/yourorg/passage-status-adapter/proto/adapter""google.golang.org/grpc")type statusServer struct {pb.UnimplementedStatusServer}func (s *statusServer) GetStatus(ctx context.Context, req *pb.StatusRequest) (*pb.StatusResponse, error) {return &pb.StatusResponse{Status: &pb.StatusData{Version: &pb.ProtocolVersion{Name: "My Network",Protocol: int32(req.Protocol),},Players: &pb.Players{Online: 42,Max: 100,},Description: stringPtr(`{"text":"Welcome!","color":"gold"}`),},}, nil}func stringPtr(s string) *string { return &s }func main() {lis, _ := net.Listen("tcp", ":50051")s := grpc.NewServer()pb.RegisterStatusServer(s, &statusServer{})log.Println("Status adapter listening on :50051")s.Serve(lis)} -
Configure Passage
routes:- hostname: "mc.example.net"status:type: grpcaddress: "http://localhost:50051"
Example: Discovery Adapter in Python
Section titled “Example: Discovery Adapter in Python”Implement a discovery adapter that returns servers from your infrastructure:
import grpcfrom concurrent import futuresimport adapter.discovery_pb2 as discovery_pb2import adapter.discovery_pb2_grpc as discovery_grpcimport adapter.adapter_pb2 as adapter_pb2
class DiscoveryService(discovery_grpc.DiscoveryServicer): def GetTargets(self, request, context): # request.client contains ClientInfo (client_address, server_address, protocol_version) servers = [ {"id": "hub-1", "host": "10.0.1.10", "port": 25565, "meta": {"type": "hub", "players": "15", "status": "online"}}, {"id": "hub-2", "host": "10.0.1.11", "port": 25565, "meta": {"type": "hub", "players": "38", "status": "online"}}, ]
targets = [] for s in servers: targets.append(adapter_pb2.Target( identifier=s["id"], address=adapter_pb2.Address(hostname=s["host"], port=s["port"]), meta=[adapter_pb2.MetaEntry(key=k, value=v) for k, v in s["meta"].items()], ))
return discovery_pb2.TargetsResponse(targets=targets)
def serve(): server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) discovery_grpc.add_DiscoveryServicer_to_server(DiscoveryService(), server) server.add_insecure_port("[::]:50051") server.start() server.wait_for_termination()
if __name__ == "__main__": serve()Configuration:
routes:- hostname: "mc.example.net" discovery: type: grpc_discovery address: "http://localhost:50051"Example: Discovery Action with Database Routing
Section titled “Example: Discovery Action with Database Routing”A discovery action adapter that routes players based on database preferences:
package main
import ( "context" "database/sql" "log" "net"
pb "github.com/yourorg/passage-router/proto/adapter" _ "github.com/lib/pq" "google.golang.org/grpc")
type routerServer struct { pb.UnimplementedDiscoveryActionServer db *sql.DB}
func (s *routerServer) Apply(ctx context.Context, req *pb.ApplyRequest) (*pb.ApplyResponse, error) { // Get player's preferred region from database var preferredRegion string err := s.db.QueryRowContext(ctx, "SELECT preferred_region FROM player_preferences WHERE uuid = $1", req.Player.Id, ).Scan(&preferredRegion)
if err != nil { // No preference found, return all targets unchanged return &pb.ApplyResponse{ Reason: &pb.ApplyResponse_Targets{ Targets: &pb.Targets{Targets: req.Targets}, }, }, nil }
// Filter to preferred region var filtered []*pb.Target for _, target := range req.Targets { if getMetadata(target, "region") == preferredRegion { filtered = append(filtered, target) } }
// Fall back to all targets if none match if len(filtered) == 0 { filtered = req.Targets }
return &pb.ApplyResponse{ Reason: &pb.ApplyResponse_Targets{ Targets: &pb.Targets{Targets: filtered}, }, }, nil}
func getMetadata(target *pb.Target, key string) string { for _, entry := range target.Meta { if entry.Key == key { return entry.Value } } return ""}
func main() { db, _ := sql.Open("postgres", "postgresql://user:pass@localhost/passage?sslmode=disable") defer db.Close()
lis, _ := net.Listen("tcp", ":50051") s := grpc.NewServer() pb.RegisterDiscoveryActionServer(s, &routerServer{db: db}) log.Println("Router adapter listening on :50051") s.Serve(lis)}Configuration:
routes:- hostname: "mc.example.net" discovery: type: dns_discovery domain: "servers.example.net" record_type: srv actions: - type: meta_filter name: "online-filter" rules: - key: "status" op: equals value: "online" - type: grpc name: "region-router" address: "http://localhost:50051" - type: player_fill_strategy name: "fill" field: "players" max_players: 50Example: Authentication Adapter
Section titled “Example: Authentication Adapter”A custom authentication adapter for integration with your own account system:
func (s *authServer) Authenticate(ctx context.Context, req *pb.AuthenticationRequest) (*pb.AuthenticationResponse, error) { // req.Player contains name and id // req.SharedSecret and req.EncodedPublic contain encryption data // req.Client contains client/server addresses and protocol version
// Verify the player against your account system profile, err := s.verifyPlayer(ctx, req.Player.Name, req.SharedSecret) if err != nil { // Reject: return a localization key for the disconnect message return &pb.AuthenticationResponse{ Reason: &pb.AuthenticationResponse_Key{ Key: "disconnect_unauthenticated", }, }, nil }
// Accept: return the player's profile return &pb.AuthenticationResponse{ Reason: &pb.AuthenticationResponse_Profile{ Profile: &pb.Profile{ Id: profile.UUID, Name: profile.Name, Properties: []*pb.ProfileProperty{ {Name: "textures", Value: profile.TextureData, Signature: &profile.Signature}, }, }, }, }, nil}Configuration:
routes:- hostname: "mc.example.net" authentication: type: grpc address: "http://localhost:50051"Rejecting Connections
Section titled “Rejecting Connections”Both the Authentication and DiscoveryAction services can reject players by returning a key instead of a successful response. The key is a localization key that Passage resolves using the route’s localization adapter:
// Reject in DiscoveryActionreturn &pb.ApplyResponse{ Reason: &pb.ApplyResponse_Key{Key: "disconnect_queue_full"},}, nil
// Reject in Authenticationreturn &pb.AuthenticationResponse{ Reason: &pb.AuthenticationResponse_Key{Key: "disconnect_unauthenticated"},}, nilThe player sees the localized message for that key in their client language.
Testing with grpcurl
Section titled “Testing with grpcurl”Test your adapters using grpcurl:
# Test Status adaptergrpcurl -plaintext -import-path ./proto -proto adapter/status.proto \ -d '{"client_address":{"hostname":"127.0.0.1","port":12345},"server_address":{"hostname":"localhost","port":25565},"protocol":769}' \ localhost:50051 scrayosnet.passage.adapter.Status/GetStatus
# Test Discovery adaptergrpcurl -plaintext -import-path ./proto -proto adapter/discovery.proto \ -d '{"client":{"client_address":{"hostname":"127.0.0.1","port":12345},"server_address":{"hostname":"localhost","port":25565},"protocol_version":769}}' \ localhost:50051 scrayosnet.passage.adapter.Discovery/GetTargets
# Test DiscoveryAction adaptergrpcurl -plaintext -import-path ./proto -proto adapter/discovery_action.proto \ -d '{"client":{"client_address":{"hostname":"127.0.0.1","port":12345}},"player":{"name":"Steve","id":"069a79f4-44e9-4726-a5be-fca90e38aaf5"},"targets":[{"identifier":"hub-1","address":{"hostname":"10.0.1.10","port":25565}}]}' \ localhost:50051 scrayosnet.passage.adapter.DiscoveryAction/ApplyBest Practices
Section titled “Best Practices”Performance
Section titled “Performance”- Keep response times under 50ms — slow adapters delay player connections
- Use connection pooling for database and HTTP connections
- Cache expensive lookups where possible
Reliability
Section titled “Reliability”- Always return a response — never leave Passage waiting
- Handle errors gracefully — return default values or reject with a meaningful localization key
- Implement health checks using the gRPC health checking protocol
Deployment
Section titled “Deployment”- Run close to Passage to minimize network latency
- Use TLS (
https://addresses) in production - Monitor response times with your observability stack
Debugging
Section titled “Debugging”Enable Passage debug logs to see adapter communication:
RUST_LOG=passage=debug passage