Skip to main content

Porting from gRPC

This guide maps gRPC concepts to their Aster equivalents and walks through a migration step by step. The conceptual mappings are language-agnostic; code examples use Python.

Conceptual mapping

gRPCAsterNotes
.proto file@service / @rpc decorated classNo code generation step. Types are defined inline.
Protocol BuffersFory (XLANG mode)Cross-language binary serialization with @wire_type tags.
protoc code generatorNot neededAster is code-first. The class definition is the contract.
gRPC channelAster endpoint (NodeAddr)Identity-addressed, not hostname-addressed.
HTTP/2 transportQUIC transportNAT-traversing, peer-to-peer capable.
Load balancerDirect peer-to-peerNo infrastructure required for connectivity.
TLS certificatesEndpoint identity (ed25519)Built into the transport layer. No separate PKI.
Service mesh sidecarNot neededAdmission, auth, and observability are built in.

Step 1: Replace .proto definitions with decorated classes

Before (gRPC):

// greeter.proto
syntax = "proto3";

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
rpc StreamGreetings (HelloRequest) returns (stream HelloReply);
}
# Generated by protoc -- do not edit
from greeter_pb2 import HelloRequest, HelloReply
from greeter_pb2_grpc import GreeterServicer

class GreeterImpl(GreeterServicer):
def SayHello(self, request, context):
return HelloReply(message=f"Hello, {request.name}!")

def StreamGreetings(self, request, context):
for i in range(5):
yield HelloReply(message=f"Hello #{i}, {request.name}!")

After (Aster):

from dataclasses import dataclass
from typing import AsyncIterator

from aster.codec import wire_type
from aster.decorators import service, rpc, server_stream

@wire_type("myapp/HelloRequest")
@dataclass
class HelloRequest:
name: str = ""

@wire_type("myapp/HelloReply")
@dataclass
class HelloReply:
message: str = ""

@service
class Greeter:

@rpc
async def say_hello(self, req: HelloRequest) -> HelloReply:
return HelloReply(message=f"Hello, {req.name}!")

@server_stream
async def stream_greetings(self, req: HelloRequest) -> AsyncIterator[HelloReply]:
for i in range(5):
yield HelloReply(message=f"Hello #{i}, {req.name}!")

No .proto file. No protoc step. No generated code. The class definition is the source of truth.

Step 2: Replace generated stubs with AsterClient

Before (gRPC):

import grpc
from greeter_pb2 import HelloRequest
from greeter_pb2_grpc import GreeterStub

channel = grpc.insecure_channel("localhost:50051")
stub = GreeterStub(channel)
response = stub.SayHello(HelloRequest(name="World"))
print(response.message)

After (Aster):

from aster import AsterClient

async with AsterClient(endpoint_addr="<base64 NodeAddr>") as client:
greeter = await client.client(Greeter)
resp = await greeter.say_hello(HelloRequest(name="World"))
print(resp.message)

The client stub is generated at runtime from the @service-decorated class. There is no separate stub package to generate or distribute.

Step 3: Replace gRPC server with AsterServer

Before (gRPC):

import grpc
from concurrent import futures
from greeter_pb2_grpc import add_GreeterServicer_to_server

server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
add_GreeterServicer_to_server(GreeterImpl(), server)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()

After (Aster):

from aster import AsterServer

async with AsterServer(services=[Greeter()]) as srv:
print(f"Listening at: {srv.endpoint_addr_b64}")
await srv.serve()

What changes

No code generation

The @service class is the contract. There is no .proto file, no protoc step, and no generated stubs to distribute.

No HTTP/2

Aster runs over QUIC (RFC 9000) on UDP. Connections traverse NATs via relay servers without port forwarding or load balancer configuration.

No load balancers

Peers connect directly by identity. The endpoint_addr encodes the node's public key and relay hints, not a hostname. Connections are authenticated at the transport layer by the node's ed25519 identity key.

No separate TLS setup

QUIC connections are encrypted by default. The node's identity key provides mutual authentication. No certificate authorities or certificate rotation workflows.

What stays the same

Four RPC patterns

Aster supports the same four patterns as gRPC: unary, server streaming, client streaming, and bidirectional streaming. The semantics are identical.

Interceptors

Aster has a middleware chain equivalent to gRPC interceptors. Built-in interceptors cover deadlines, retry, circuit breaking, auth, audit logging, metrics, and compression.

Deadline propagation

Method-level timeouts work the same way. Set a timeout on the method decorator or per-call. The DeadlineInterceptor propagates deadlines through the call chain.

Status codes

Aster uses the same 17 status codes (0--16) as gRPC, with compatible semantics:

CodeNamegRPC equivalent
0OKOK
1CANCELLEDCANCELLED
3INVALID_ARGUMENTINVALID_ARGUMENT
4DEADLINE_EXCEEDEDDEADLINE_EXCEEDED
5NOT_FOUNDNOT_FOUND
7PERMISSION_DENIEDPERMISSION_DENIED
13INTERNALINTERNAL
14UNAVAILABLEUNAVAILABLE
16UNAUTHENTICATEDUNAUTHENTICATED

The full set includes all 17 codes (2, 6, 8, 9, 10, 11, 12, 15 are also supported).

Streaming migration

gRPC streaming patterns map directly to Aster decorators:

gRPC patternAster decoratorPython signature
rpc Method(Req) returns (Resp)@rpcasync def method(self, req: Req) -> Resp
rpc Method(Req) returns (stream Resp)@server_streamasync def method(self, req: Req) -> AsyncIterator[Resp]
rpc Method(stream Req) returns (Resp)@client_streamasync def method(self, stream: AsyncIterator[Req]) -> Resp
rpc Method(stream Req) returns (stream Resp)@bidi_streamasync def method(self, reqs: AsyncIterator[Req]) -> AsyncIterator[Resp]

The difference: gRPC uses generated iterator types; Aster uses standard AsyncIterator from Python's typing module.

Authentication migration

gRPC authentication typically uses metadata headers and custom interceptors (or mTLS). Aster replaces this with a built-in trust model:

  • gRPC metadata/interceptors become enrollment credentials. The operator signs credentials offline with a root key. Consumers present them during the admission handshake.
  • mTLS certificates become endpoint identity keys. Each node has an ed25519 keypair embedded in the QUIC transport. No separate certificate infrastructure.
  • Authorization interceptors become capability requirements. Declare @service(requires=...) or @rpc(requires=...) and the framework enforces them at the method dispatch layer.

What you gain

  • NAT traversal. Peers behind NATs connect via relay servers without port forwarding. The relay is part of the transport, not a separate proxy.
  • Peer-to-peer. Once a direct path is established (via hole punching or local discovery), traffic flows directly between peers with no intermediary.
  • Content-addressed contracts. Each service version gets a deterministic contract_id (a hash of its interface). Consumers and producers can verify wire compatibility without running code.
  • Built-in discovery. The admission handshake returns the full service list. No separate service registry or DNS-based discovery required.
  • No infrastructure. No load balancers, no service mesh sidecars, no certificate authorities. The transport handles connectivity; the framework handles auth.