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
Section titled “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
Section titled “Step 1: Replace .proto definitions with decorated classes”Before (gRPC):
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 editfrom greeter_pb2 import HelloRequest, HelloReplyfrom 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 dataclassfrom typing import AsyncIterator
from aster import service, rpc, server_stream, wire_type
@wire_type("myapp/HelloRequest")@dataclassclass HelloRequest: name: str = ""
@wire_type("myapp/HelloReply")@dataclassclass HelloReply: message: str = ""
@serviceclass 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}!")Before (gRPC):
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 editimport { HelloRequest, HelloReply } from './greeter_pb';import { GreeterService } from './greeter_grpc_pb';
class GreeterImpl implements GreeterService { sayHello(request: HelloRequest): HelloReply { return new HelloReply({ message: `Hello, ${request.name}!` }); } // ...streaming setup requires grpc-js boilerplate}After (Aster):
import { WireType, Service, Rpc, ServerStream } from '@aster-rpc/aster';
@WireType("myapp/HelloRequest")class HelloRequest { name = ""; constructor(init?: Partial<HelloRequest>) { if (init) Object.assign(this, init); }}
@WireType("myapp/HelloReply")class HelloReply { message = ""; constructor(init?: Partial<HelloReply>) { if (init) Object.assign(this, init); }}
@Service({ name: "Greeter", version: 1 })class Greeter { @Rpc() async sayHello(req: HelloRequest): Promise<HelloReply> { return new HelloReply({ message: `Hello, ${req.name}!` }); }
@ServerStream() async *streamGreetings(req: HelloRequest): AsyncGenerator<HelloReply> { for (let i = 0; i < 5; i++) { yield new HelloReply({ message: `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 grpcfrom greeter_pb2 import HelloRequestfrom 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 grpcfrom concurrent import futuresfrom 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()What changes
Section titled “What changes”No code generation
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “What stays the same”Four RPC patterns
Section titled “Four RPC patterns”Aster supports the same four patterns as gRPC: unary, server streaming, client streaming, and bidirectional streaming. The semantics are identical.
Interceptors
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.