Skip to 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.

gRPC Aster Notes
.proto file @service / @rpc decorated class No code generation step. Types are defined inline.
Protocol Buffers Fory (XLANG mode) Cross-language binary serialization with @wire_type tags.
protoc code generator Not needed Aster is code-first. The class definition is the contract.
gRPC channel Aster endpoint (NodeAddr) Identity-addressed, not hostname-addressed.
HTTP/2 transport QUIC transport NAT-traversing, peer-to-peer capable.
Load balancer Direct peer-to-peer No infrastructure required for connectivity.
TLS certificates Endpoint identity (ed25519) Built into the transport layer. No separate PKI.
Service mesh sidecar Not needed Admission, auth, and observability are built in.

Step 1: Replace .proto definitions with decorated classes

Section titled “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 import service, rpc, server_stream, wire_type
@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

Section titled “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(address="aster1...") 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

Section titled “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.address}")
await srv.serve()

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

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

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.

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

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

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

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.

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

Code Name gRPC equivalent
0 OK OK
1 CANCELLED CANCELLED
3 INVALID_ARGUMENT INVALID_ARGUMENT
4 DEADLINE_EXCEEDED DEADLINE_EXCEEDED
5 NOT_FOUND NOT_FOUND
7 PERMISSION_DENIED PERMISSION_DENIED
13 INTERNAL INTERNAL
14 UNAVAILABLE UNAVAILABLE
16 UNAUTHENTICATED UNAUTHENTICATED

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

gRPC streaming patterns map directly to Aster decorators:

gRPC pattern Aster decorator Python signature
rpc Method(Req) returns (Resp) @rpc async def method(self, req: Req) -> Resp
rpc Method(Req) returns (stream Resp) @server_stream async def method(self, req: Req) -> AsyncIterator[Resp]
rpc Method(stream Req) returns (Resp) @client_stream async def method(self, stream: AsyncIterator[Req]) -> Resp
rpc Method(stream Req) returns (stream Resp) @bidi_stream async def method(self, reqs: AsyncIterator[Req]) -> AsyncIterator[Resp]

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

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.
  • 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.