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
| 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
- Python
- Rust
- JVM
- .NET
- Go
- JavaScript
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}!")
// Rust binding planned. The migration will follow the same pattern:
// replace .proto with annotated structs + trait impl.
// JVM binding planned. The migration will follow the same pattern:
// replace .proto with annotated classes + interface impl.
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:
| 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).
Streaming migration
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.
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.