Skip to content

gRPC Transport

FraiseQL’s gRPC transport generates a Protobuf API from your compiled schema. The compiler produces transport-aware database views that skip JSON serialization entirely — typed columns map directly to protobuf fields with no JSON intermediate.

Requires the grpc-transport Cargo feature.

The compiler generates the optimal view shape for each transport from the same SDK annotation:

TransportView shapePostgreSQL workServer workWire format
GraphQLJSON-shaped (json_agg, row_to_json)JSON serializationPass-throughJSON
RESTSame JSON viewsJSON serializationUnwrap + re-keyJSON
gRPCRow-shaped (typed columns)No JSON workColumn → protobuf fieldBinary protobuf

The gRPC path is faster end-to-end because:

  1. PostgreSQL skips json_agg / row_to_json (not free, especially for nested objects)
  2. The server never parses JSON — typed columns map directly to protobuf fields
  3. Protobuf wire format is smaller (no key names, varint integers, no string escaping)

Honest framing: gRPC performance is competitive with hand-written tonic, not faster — FraiseQL’s gRPC transport is built on tonic. The real win is zero handler code with near-native performance.

gRPC endpoints require no SDK annotations. When gRPC is enabled, the compiler automatically generates a .proto service with one RPC per query and mutation. Method names follow naming conventions:

Schema operationGenerated gRPC RPCRule
get_user queryGetUserPascalCase of query name
posts list queryListPosts (server-streaming)List + PascalCase for list returns
create_user mutationCreateUserMutationResponsePascalCase of mutation name

All types are included by default. Use include_types / exclude_types in [grpc] to filter which types (and their associated queries/mutations) are exposed.

The .proto file is auto-generated from your compiled schema. Retrieve it with:

Terminal window
fraiseql compile --output-proto schema.proto

Use the generated .proto to generate client code in any gRPC-supported language:

Terminal window
# Go
protoc --go_out=. --go-grpc_out=. schema.proto
# Python
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. schema.proto
# TypeScript
grpc_tools_node_protoc --ts_out=. schema.proto
FraiseQL typeProtobuf type
Stringstring
Intint32
BigIntint64
Floatdouble
Booleanbool
ID / UUIDstring
DateTimegoogle.protobuf.Timestamp
JSONgoogle.protobuf.Struct
Decimalstring (preserves precision)
[T]repeated T
nullable Toptional T
Enumenum
Object typemessage

Custom scalars not in this table are serialized as string.

GraphQL is nullable-by-default; protobuf3 uses optional for nullable fields. The compiler translates:

GraphQL: name: String → proto: optional string name = 1;
GraphQL: name: String! → proto: string name = 1;

Nested object fields compile to protobuf message types. The row-shaped view uses JOINs returning flat columns; the server reconstructs the message nesting during serialization.

Mutations are automatically exposed as gRPC RPCs that call the underlying database function via execute_function_call(). The response is a standard MutationResponse message:

message MutationResponse {
bool success = 1;
optional string id = 2; // entity ID returned by the function
optional string error = 3;
}

Given a mutation create_user, the compiler generates a CreateUserRequest message from the function arguments and a CreateUser RPC that returns MutationResponse. No additional annotation is needed.

List queries are automatically exposed as server-streaming RPCs. When a query returns a list, the generated .proto uses returns (stream Entity) instead of a wrapper response — rows are fetched in batches from the database and streamed individually as gRPC frames:

// Generated for a list query
rpc ListPosts(ListPostsRequest) returns (stream Post);
// Generated for a single-item query
rpc GetUser(GetUserRequest) returns (User);

The batch size is controlled by the stream_batch_size config field (default 500). Memory usage is O(batch_size) regardless of total result count.

Subscriptions also map to server-streaming RPCs. A @fraiseql.subscription decorator generates both a WebSocket subscription and a gRPC server-streaming RPC automatically — the same subscription logic powers both transports.

The server uses PostgreSQL LISTEN/NOTIFY (same as GraphQL subscriptions) to push events through the gRPC stream. Client-streaming and bidirectional streaming are not supported.

[grpc]
enabled = true
port = 50052 # separate port for gRPC (default 50052)
reflection = true # gRPC reflection for grpcurl/gRPC UI (default true)
max_message_size_bytes = 4194304 # 4 MiB (default)
descriptor_path = "proto/descriptor.binpb" # compiled FileDescriptorSet
stream_batch_size = 500 # rows per batch in server-streaming RPCs
# include_types = ["User", "Post"] # whitelist (empty = all types)
# exclude_types = ["InternalAudit"] # blacklist

gRPC runs on a separate port (default 50052) from the HTTP server. This avoids HTTP/2 negotiation complexity and lets you expose gRPC only on internal networks while keeping HTTP public.

FieldTypeDefaultDescription
enabledboolfalseEnable gRPC transport
portinteger50052gRPC listen port
reflectionbooltrueEnable gRPC server reflection
max_message_size_bytesinteger4194304Maximum inbound message size (bytes)
descriptor_pathstring"proto/descriptor.binpb"Path to compiled FileDescriptorSet binary
stream_batch_sizeinteger500Rows per batch in server-streaming RPCs
include_typesstring[][]Whitelist of type names to expose (empty = all)
exclude_typesstring[][]Blacklist of type names to hide

gRPC transport requires the grpc-transport Cargo feature at compile time. The default ghcr.io/fraiseql/server:latest image does not include this feature — use ghcr.io/fraiseql/server-full:latest (all features), or build from source (see GitHub issue #80):

Terminal window
cargo build --release --features grpc-transport

After generating client code from the .proto:

Terminal window
grpcurl -plaintext \
-d '{"id": "123"}' \
localhost:50052 \
UserService/GetUser

gRPC is built on tonic, which integrates with axum via shared Tower middleware. Auth, rate limiting, tracing, and all other middleware apply to gRPC requests unchanged.

Generate proto files for client code generation:

Terminal window
fraiseql compile schema.py --output-proto ./proto/

The generated .proto reflects the exact API shape from your compiled schema — no manual proto authoring needed.

The reflection service is enabled by default when grpc-transport is active. Tools like grpcurl and gRPC UI can discover services without a .proto file:

Terminal window
# List all services
grpcurl -plaintext localhost:50052 list
# Describe a specific service
grpcurl -plaintext localhost:50052 describe UserService

gRPC auth follows standard gRPC conventions — pass credentials in metadata headers:

Terminal window
grpcurl -plaintext \
-H "authorization: Bearer $TOKEN" \
-d '{"id": "123"}' \
localhost:50052 \
UserService/GetUser

The same JWT, OIDC, and API key auth configured for GraphQL and REST applies to gRPC with no additional setup.